@deeplake/hivemind 0.6.47 → 0.7.4

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 (41) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +158 -51
  4. package/bundle/cli.js +4103 -282
  5. package/codex/bundle/capture.js +510 -90
  6. package/codex/bundle/commands/auth-login.js +219 -72
  7. package/codex/bundle/embeddings/embed-daemon.js +243 -0
  8. package/codex/bundle/pre-tool-use.js +713 -108
  9. package/codex/bundle/session-start-setup.js +209 -58
  10. package/codex/bundle/session-start.js +40 -11
  11. package/codex/bundle/shell/deeplake-shell.js +679 -112
  12. package/codex/bundle/stop.js +477 -59
  13. package/codex/bundle/wiki-worker.js +312 -11
  14. package/cursor/bundle/capture.js +768 -57
  15. package/cursor/bundle/commands/auth-login.js +219 -72
  16. package/cursor/bundle/embeddings/embed-daemon.js +243 -0
  17. package/cursor/bundle/pre-tool-use.js +1684 -0
  18. package/cursor/bundle/session-end.js +223 -2
  19. package/cursor/bundle/session-start.js +209 -57
  20. package/cursor/bundle/shell/deeplake-shell.js +679 -112
  21. package/cursor/bundle/wiki-worker.js +571 -0
  22. package/hermes/bundle/capture.js +1194 -0
  23. package/hermes/bundle/commands/auth-login.js +1009 -0
  24. package/hermes/bundle/embeddings/embed-daemon.js +243 -0
  25. package/hermes/bundle/package.json +1 -0
  26. package/hermes/bundle/pre-tool-use.js +1681 -0
  27. package/hermes/bundle/session-end.js +265 -0
  28. package/hermes/bundle/session-start.js +655 -0
  29. package/hermes/bundle/shell/deeplake-shell.js +69905 -0
  30. package/hermes/bundle/wiki-worker.js +572 -0
  31. package/mcp/bundle/server.js +289 -69
  32. package/openclaw/dist/chunks/auth-creds-AEKS6D3P.js +14 -0
  33. package/openclaw/dist/chunks/chunk-SRCBBT4H.js +37 -0
  34. package/openclaw/dist/chunks/config-G23NI5TV.js +33 -0
  35. package/openclaw/dist/chunks/index-marker-store-PGT5CW6T.js +33 -0
  36. package/openclaw/dist/chunks/setup-config-C35UK4LP.js +114 -0
  37. package/openclaw/dist/index.js +752 -702
  38. package/openclaw/openclaw.plugin.json +1 -1
  39. package/openclaw/package.json +1 -1
  40. package/package.json +7 -3
  41. package/pi/extension-source/hivemind.ts +807 -0
@@ -1,164 +1,24 @@
1
- // openclaw/src/setup-config.ts
2
- import { existsSync, readFileSync, writeFileSync, renameSync } from "node:fs";
3
- import { homedir } from "node:os";
4
- import { join } from "node:path";
5
- var HIVEMIND_TOOL_NAMES = ["hivemind_search", "hivemind_read", "hivemind_index"];
6
- function getOpenclawConfigPath() {
7
- return join(homedir(), ".openclaw", "openclaw.json");
8
- }
9
- function isAllowlistCoveringHivemind(alsoAllow) {
10
- if (!Array.isArray(alsoAllow)) return false;
11
- for (const entry of alsoAllow) {
12
- if (typeof entry !== "string") continue;
13
- const normalized = entry.trim().toLowerCase();
14
- if (normalized === "hivemind") return true;
15
- if (normalized === "group:plugins") return true;
16
- if (HIVEMIND_TOOL_NAMES.includes(normalized)) return true;
17
- }
18
- return false;
19
- }
20
- function ensureHivemindAllowlisted() {
21
- const configPath = getOpenclawConfigPath();
22
- if (!existsSync(configPath)) {
23
- return { status: "error", configPath, error: "openclaw config file not found" };
24
- }
25
- let parsed;
26
- try {
27
- const raw = readFileSync(configPath, "utf-8");
28
- parsed = JSON.parse(raw);
29
- } catch (e) {
30
- return { status: "error", configPath, error: `could not read/parse config: ${e instanceof Error ? e.message : String(e)}` };
31
- }
32
- const tools = parsed.tools ?? {};
33
- const alsoAllow = Array.isArray(tools.alsoAllow) ? tools.alsoAllow : [];
34
- if (isAllowlistCoveringHivemind(alsoAllow)) {
35
- return { status: "already-set", configPath };
36
- }
37
- const updated = {
38
- ...parsed,
39
- tools: {
40
- ...tools,
41
- alsoAllow: [...alsoAllow, "hivemind"]
42
- }
43
- };
44
- const backupPath = `${configPath}.bak-hivemind-${Date.now()}`;
45
- const tmpPath = `${configPath}.tmp-hivemind-${process.pid}`;
46
- try {
47
- writeFileSync(backupPath, readFileSync(configPath, "utf-8"));
48
- writeFileSync(tmpPath, JSON.stringify(updated, null, 2) + "\n");
49
- renameSync(tmpPath, configPath);
50
- } catch (e) {
51
- return { status: "error", configPath, error: `could not write config: ${e instanceof Error ? e.message : String(e)}` };
52
- }
53
- return { status: "added", configPath, backupPath };
54
- }
55
- function toggleAutoUpdateConfig(setTo) {
56
- const configPath = getOpenclawConfigPath();
57
- if (!existsSync(configPath)) {
58
- return { status: "error", configPath, error: "openclaw config file not found" };
59
- }
60
- let parsed;
61
- try {
62
- parsed = JSON.parse(readFileSync(configPath, "utf-8"));
63
- } catch (e) {
64
- return { status: "error", configPath, error: `could not read/parse config: ${e instanceof Error ? e.message : String(e)}` };
65
- }
66
- const plugins = parsed.plugins ?? {};
67
- const entries = plugins.entries ?? {};
68
- const hivemindEntry = entries.hivemind ?? {};
69
- const pluginConfig = hivemindEntry.config ?? {};
70
- const current = pluginConfig.autoUpdate !== false;
71
- const newValue = typeof setTo === "boolean" ? setTo : !current;
72
- const updated = {
73
- ...parsed,
74
- plugins: {
75
- ...plugins,
76
- entries: {
77
- ...entries,
78
- hivemind: {
79
- ...hivemindEntry,
80
- config: { ...pluginConfig, autoUpdate: newValue }
81
- }
82
- }
83
- }
84
- };
85
- const backupPath = `${configPath}.bak-hivemind-${Date.now()}`;
86
- const tmpPath = `${configPath}.tmp-hivemind-${process.pid}`;
87
- try {
88
- writeFileSync(backupPath, readFileSync(configPath, "utf-8"));
89
- writeFileSync(tmpPath, JSON.stringify(updated, null, 2) + "\n");
90
- renameSync(tmpPath, configPath);
91
- } catch (e) {
92
- return { status: "error", configPath, error: `could not write config: ${e instanceof Error ? e.message : String(e)}` };
93
- }
94
- return { status: "updated", configPath, newValue };
95
- }
96
- function detectAllowlistMissing() {
97
- const configPath = getOpenclawConfigPath();
98
- if (!existsSync(configPath)) return false;
99
- try {
100
- const parsed = JSON.parse(readFileSync(configPath, "utf-8"));
101
- const tools = parsed.tools ?? {};
102
- return !isAllowlistCoveringHivemind(tools.alsoAllow);
103
- } catch {
104
- return false;
105
- }
106
- }
1
+ import {
2
+ loadCredentials,
3
+ saveCredentials
4
+ } from "./chunks/chunk-SRCBBT4H.js";
107
5
 
108
- // src/config.ts
109
- import { readFileSync as readFileSync2, existsSync as existsSync2 } from "node:fs";
110
- import { join as join2 } from "node:path";
111
- import { homedir as homedir2, userInfo } from "node:os";
112
- function loadConfig() {
113
- const home = homedir2();
114
- const credPath = join2(home, ".deeplake", "credentials.json");
115
- let creds = null;
116
- if (existsSync2(credPath)) {
117
- try {
118
- creds = JSON.parse(readFileSync2(credPath, "utf-8"));
119
- } catch {
120
- return null;
121
- }
122
- }
123
- const token = creds?.token;
124
- const orgId = creds?.orgId;
125
- if (!token || !orgId) return null;
126
- return {
127
- token,
128
- orgId,
129
- orgName: creds?.orgName ?? orgId,
130
- userName: creds?.userName || userInfo().username || "unknown",
131
- workspaceId: creds?.workspaceId ?? "default",
132
- apiUrl: creds?.apiUrl ?? "https://api.deeplake.ai",
133
- tableName: "memory",
134
- sessionsTableName: "sessions",
135
- memoryPath: join2(home, ".deeplake", "memory")
136
- };
6
+ // src/utils/client-header.ts
7
+ var DEEPLAKE_CLIENT_HEADER = "X-Deeplake-Client";
8
+ function deeplakeClientValue() {
9
+ return "hivemind";
10
+ }
11
+ function deeplakeClientHeader() {
12
+ return { [DEEPLAKE_CLIENT_HEADER]: deeplakeClientValue() };
137
13
  }
138
14
 
139
15
  // src/commands/auth.ts
140
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync3, mkdirSync, unlinkSync } from "node:fs";
141
- import { join as join3 } from "node:path";
142
- import { homedir as homedir3 } from "node:os";
143
- var CONFIG_DIR = join3(homedir3(), ".deeplake");
144
- var CREDS_PATH = join3(CONFIG_DIR, "credentials.json");
145
16
  var DEFAULT_API_URL = "https://api.deeplake.ai";
146
- function loadCredentials() {
147
- if (!existsSync3(CREDS_PATH)) return null;
148
- try {
149
- return JSON.parse(readFileSync3(CREDS_PATH, "utf-8"));
150
- } catch {
151
- return null;
152
- }
153
- }
154
- function saveCredentials(creds) {
155
- if (!existsSync3(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
156
- writeFileSync2(CREDS_PATH, JSON.stringify({ ...creds, savedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2), { mode: 384 });
157
- }
158
17
  async function apiGet(path, token, apiUrl, orgId) {
159
18
  const headers = {
160
19
  Authorization: `Bearer ${token}`,
161
- "Content-Type": "application/json"
20
+ "Content-Type": "application/json",
21
+ ...deeplakeClientHeader()
162
22
  };
163
23
  if (orgId) headers["X-Activeloop-Org-Id"] = orgId;
164
24
  const resp = await fetch(`${apiUrl}${path}`, { headers });
@@ -168,7 +28,7 @@ async function apiGet(path, token, apiUrl, orgId) {
168
28
  async function requestDeviceCode(apiUrl = DEFAULT_API_URL) {
169
29
  const resp = await fetch(`${apiUrl}/auth/device/code`, {
170
30
  method: "POST",
171
- headers: { "Content-Type": "application/json" }
31
+ headers: { "Content-Type": "application/json", ...deeplakeClientHeader() }
172
32
  });
173
33
  if (!resp.ok) throw new Error(`Device flow unavailable: HTTP ${resp.status}`);
174
34
  return resp.json();
@@ -176,7 +36,7 @@ async function requestDeviceCode(apiUrl = DEFAULT_API_URL) {
176
36
  async function pollForToken(deviceCode, apiUrl = DEFAULT_API_URL) {
177
37
  const resp = await fetch(`${apiUrl}/auth/device/token`, {
178
38
  method: "POST",
179
- headers: { "Content-Type": "application/json" },
39
+ headers: { "Content-Type": "application/json", ...deeplakeClientHeader() },
180
40
  body: JSON.stringify({ device_code: deviceCode })
181
41
  });
182
42
  if (resp.ok) return resp.json();
@@ -210,16 +70,13 @@ async function switchWorkspace(workspaceId) {
210
70
 
211
71
  // src/deeplake-api.ts
212
72
  import { randomUUID } from "node:crypto";
213
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
214
- import { join as join5 } from "node:path";
215
- import { tmpdir } from "node:os";
216
73
 
217
74
  // src/utils/debug.ts
218
75
  import { appendFileSync } from "node:fs";
219
- import { join as join4 } from "node:path";
220
- import { homedir as homedir4 } from "node:os";
76
+ import { join } from "node:path";
77
+ import { homedir } from "node:os";
221
78
  var DEBUG = false;
222
- var LOG = join4(homedir4(), ".deeplake", "hook-debug.log");
79
+ var LOG = join(homedir(), ".deeplake", "hook-debug.log");
223
80
  function log(tag, msg) {
224
81
  if (!DEBUG) return;
225
82
  appendFileSync(LOG, `${(/* @__PURE__ */ new Date()).toISOString()} [${tag}] ${msg}
@@ -234,7 +91,16 @@ function sqlLike(value) {
234
91
  return sqlStr(value).replace(/%/g, "\\%").replace(/_/g, "\\_");
235
92
  }
236
93
 
94
+ // src/embeddings/columns.ts
95
+ var SUMMARY_EMBEDDING_COL = "summary_embedding";
96
+ var MESSAGE_EMBEDDING_COL = "message_embedding";
97
+
237
98
  // src/deeplake-api.ts
99
+ var indexMarkerStorePromise = null;
100
+ function getIndexMarkerStore() {
101
+ if (!indexMarkerStorePromise) indexMarkerStorePromise = import("./chunks/index-marker-store-PGT5CW6T.js");
102
+ return indexMarkerStorePromise;
103
+ }
238
104
  var log2 = (msg) => log("sdk", msg);
239
105
  function summarizeSql(sql, maxLen = 220) {
240
106
  const compact = sql.replace(/\s+/g, " ").trim();
@@ -252,7 +118,6 @@ var MAX_RETRIES = 3;
252
118
  var BASE_DELAY_MS = 500;
253
119
  var MAX_CONCURRENCY = 5;
254
120
  var QUERY_TIMEOUT_MS = Number(1e4);
255
- var INDEX_MARKER_TTL_MS = Number(6 * 60 * 6e4);
256
121
  function sleep(ms) {
257
122
  return new Promise((resolve) => setTimeout(resolve, ms));
258
123
  }
@@ -272,9 +137,6 @@ function isTransientHtml403(text) {
272
137
  const body = text.toLowerCase();
273
138
  return body.includes("<html") || body.includes("403 forbidden") || body.includes("cloudflare") || body.includes("nginx");
274
139
  }
275
- function getIndexMarkerDir() {
276
- return join5(tmpdir(), "hivemind-deeplake-indexes");
277
- }
278
140
  var Semaphore = class {
279
141
  constructor(max) {
280
142
  this.max = max;
@@ -343,7 +205,8 @@ var DeeplakeApi = class {
343
205
  headers: {
344
206
  Authorization: `Bearer ${this.token}`,
345
207
  "Content-Type": "application/json",
346
- "X-Activeloop-Org-Id": this.orgId
208
+ "X-Activeloop-Org-Id": this.orgId,
209
+ ...deeplakeClientHeader()
347
210
  },
348
211
  signal,
349
212
  body: JSON.stringify({ query: sql })
@@ -371,7 +234,8 @@ var DeeplakeApi = class {
371
234
  }
372
235
  const text = await resp.text().catch(() => "");
373
236
  const retryable403 = isSessionInsertQuery(sql) && (resp.status === 401 || resp.status === 403 && (text.length === 0 || isTransientHtml403(text)));
374
- if (attempt < MAX_RETRIES && (RETRYABLE_CODES.has(resp.status) || retryable403)) {
237
+ const alreadyExists = resp.status === 500 && isDuplicateIndexError(text);
238
+ if (!alreadyExists && attempt < MAX_RETRIES && (RETRYABLE_CODES.has(resp.status) || retryable403)) {
375
239
  const delay = BASE_DELAY_MS * Math.pow(2, attempt) + Math.random() * 200;
376
240
  log2(`query retry ${attempt + 1}/${MAX_RETRIES} (${resp.status}) in ${delay.toFixed(0)}ms`);
377
241
  await sleep(delay);
@@ -406,7 +270,7 @@ var DeeplakeApi = class {
406
270
  `SELECT path FROM "${this.tableName}" WHERE path = '${sqlStr(row.path)}' LIMIT 1`
407
271
  );
408
272
  if (exists.length > 0) {
409
- let setClauses = `summary = E'${sqlStr(row.contentText)}', mime_type = '${sqlStr(row.mimeType)}', size_bytes = ${row.sizeBytes}, last_update_date = '${lud}'`;
273
+ let setClauses = `summary = E'${sqlStr(row.contentText)}', ${SUMMARY_EMBEDDING_COL} = NULL, mime_type = '${sqlStr(row.mimeType)}', size_bytes = ${row.sizeBytes}, last_update_date = '${lud}'`;
410
274
  if (row.project !== void 0) setClauses += `, project = '${sqlStr(row.project)}'`;
411
275
  if (row.description !== void 0) setClauses += `, description = '${sqlStr(row.description)}'`;
412
276
  await this.query(
@@ -414,8 +278,8 @@ var DeeplakeApi = class {
414
278
  );
415
279
  } else {
416
280
  const id = randomUUID();
417
- let cols = "id, path, filename, summary, mime_type, size_bytes, creation_date, last_update_date";
418
- let vals = `'${id}', '${sqlStr(row.path)}', '${sqlStr(row.filename)}', E'${sqlStr(row.contentText)}', '${sqlStr(row.mimeType)}', ${row.sizeBytes}, '${cd}', '${lud}'`;
281
+ let cols = `id, path, filename, summary, ${SUMMARY_EMBEDDING_COL}, mime_type, size_bytes, creation_date, last_update_date`;
282
+ let vals = `'${id}', '${sqlStr(row.path)}', '${sqlStr(row.filename)}', E'${sqlStr(row.contentText)}', NULL, '${sqlStr(row.mimeType)}', ${row.sizeBytes}, '${cd}', '${lud}'`;
419
283
  if (row.project !== void 0) {
420
284
  cols += ", project";
421
285
  vals += `, '${sqlStr(row.project)}'`;
@@ -444,49 +308,79 @@ var DeeplakeApi = class {
444
308
  buildLookupIndexName(table, suffix) {
445
309
  return `idx_${table}_${suffix}`.replace(/[^a-zA-Z0-9_]/g, "_");
446
310
  }
447
- getLookupIndexMarkerPath(table, suffix) {
448
- const markerKey = [
449
- this.workspaceId,
450
- this.orgId,
451
- table,
452
- suffix
453
- ].join("__").replace(/[^a-zA-Z0-9_.-]/g, "_");
454
- return join5(getIndexMarkerDir(), `${markerKey}.json`);
455
- }
456
- hasFreshLookupIndexMarker(table, suffix) {
457
- const markerPath = this.getLookupIndexMarkerPath(table, suffix);
458
- if (!existsSync4(markerPath)) return false;
459
- try {
460
- const raw = JSON.parse(readFileSync4(markerPath, "utf-8"));
461
- const updatedAt = raw.updatedAt ? new Date(raw.updatedAt).getTime() : NaN;
462
- if (!Number.isFinite(updatedAt) || Date.now() - updatedAt > INDEX_MARKER_TTL_MS) return false;
463
- return true;
464
- } catch {
465
- return false;
466
- }
467
- }
468
- markLookupIndexReady(table, suffix) {
469
- mkdirSync2(getIndexMarkerDir(), { recursive: true });
470
- writeFileSync3(
471
- this.getLookupIndexMarkerPath(table, suffix),
472
- JSON.stringify({ updatedAt: (/* @__PURE__ */ new Date()).toISOString() }),
473
- "utf-8"
474
- );
475
- }
476
311
  async ensureLookupIndex(table, suffix, columnsSql) {
477
- if (this.hasFreshLookupIndexMarker(table, suffix)) return;
312
+ const markers = await getIndexMarkerStore();
313
+ const markerPath = markers.buildIndexMarkerPath(this.workspaceId, this.orgId, table, suffix);
314
+ if (markers.hasFreshIndexMarker(markerPath)) return;
478
315
  const indexName = this.buildLookupIndexName(table, suffix);
479
316
  try {
480
317
  await this.query(`CREATE INDEX IF NOT EXISTS "${indexName}" ON "${table}" ${columnsSql}`);
481
- this.markLookupIndexReady(table, suffix);
318
+ markers.writeIndexMarker(markerPath);
482
319
  } catch (e) {
483
320
  if (isDuplicateIndexError(e)) {
484
- this.markLookupIndexReady(table, suffix);
321
+ markers.writeIndexMarker(markerPath);
485
322
  return;
486
323
  }
487
324
  log2(`index "${indexName}" skipped: ${e.message}`);
488
325
  }
489
326
  }
327
+ /**
328
+ * Ensure a vector column exists on the given table.
329
+ *
330
+ * The previous implementation always issued `ALTER TABLE ADD COLUMN IF NOT
331
+ * EXISTS …` on every SessionStart. On a long-running workspace that's
332
+ * already migrated, every call returns 500 "Column already exists" — noisy
333
+ * in the log and a wasted round-trip. Worse, the very first call after the
334
+ * column is genuinely added triggers Deeplake's post-ALTER `vector::at`
335
+ * window (~30s) during which subsequent INSERTs fail; minimising the
336
+ * number of ALTER calls minimises exposure to that window.
337
+ *
338
+ * New flow:
339
+ * 1. Check the local marker file (mirrors ensureLookupIndex). If fresh,
340
+ * return — zero network calls.
341
+ * 2. SELECT 1 FROM information_schema.columns WHERE table_name = T AND
342
+ * column_name = C. Read-only, idempotent, can't tickle the post-ALTER
343
+ * bug. If the column is present → mark + return.
344
+ * 3. Only if step 2 says the column is missing, fall back to ALTER ADD
345
+ * COLUMN IF NOT EXISTS. Mark on success, also mark if Deeplake reports
346
+ * "already exists" (race: another client added it between our SELECT
347
+ * and ALTER).
348
+ *
349
+ * Marker uses the same dir / TTL as ensureLookupIndex so both schema
350
+ * caches share an opt-out (HIVEMIND_INDEX_MARKER_DIR) and a TTL knob.
351
+ */
352
+ async ensureEmbeddingColumn(table, column) {
353
+ await this.ensureColumn(table, column, "FLOAT4[]");
354
+ }
355
+ /**
356
+ * Generic marker-gated column migration. Same SELECT-then-ALTER flow as
357
+ * ensureEmbeddingColumn, parameterized by SQL type so it can patch up any
358
+ * column that was added to the schema after the table was originally
359
+ * created. Used today for `summary_embedding`, `message_embedding`, and
360
+ * the `agent` column (added 2026-04-11) — the latter has no fallback if
361
+ * a user upgraded over a pre-2026-04-11 table, so every INSERT fails
362
+ * with `column "agent" does not exist`.
363
+ */
364
+ async ensureColumn(table, column, sqlType) {
365
+ const markers = await getIndexMarkerStore();
366
+ const markerPath = markers.buildIndexMarkerPath(this.workspaceId, this.orgId, table, `col_${column}`);
367
+ if (markers.hasFreshIndexMarker(markerPath)) return;
368
+ const colCheck = `SELECT 1 FROM information_schema.columns WHERE table_name = '${sqlStr(table)}' AND column_name = '${sqlStr(column)}' AND table_schema = '${sqlStr(this.workspaceId)}' LIMIT 1`;
369
+ const rows = await this.query(colCheck);
370
+ if (rows.length > 0) {
371
+ markers.writeIndexMarker(markerPath);
372
+ return;
373
+ }
374
+ try {
375
+ await this.query(`ALTER TABLE "${table}" ADD COLUMN ${column} ${sqlType}`);
376
+ } catch (e) {
377
+ const msg = e instanceof Error ? e.message : String(e);
378
+ if (!/already exists/i.test(msg)) throw e;
379
+ const recheck = await this.query(colCheck);
380
+ if (recheck.length === 0) throw e;
381
+ }
382
+ markers.writeIndexMarker(markerPath);
383
+ }
490
384
  /** List all tables in the workspace (with retry). */
491
385
  async listTables(forceRefresh = false) {
492
386
  if (!forceRefresh && this._tablesCache) return [...this._tablesCache];
@@ -500,7 +394,8 @@ var DeeplakeApi = class {
500
394
  const resp = await fetch(`${this.apiUrl}/workspaces/${this.workspaceId}/tables`, {
501
395
  headers: {
502
396
  Authorization: `Bearer ${this.token}`,
503
- "X-Activeloop-Org-Id": this.orgId
397
+ "X-Activeloop-Org-Id": this.orgId,
398
+ ...deeplakeClientHeader()
504
399
  }
505
400
  });
506
401
  if (resp.ok) {
@@ -525,30 +420,64 @@ var DeeplakeApi = class {
525
420
  }
526
421
  return { tables: [], cacheable: false };
527
422
  }
423
+ /**
424
+ * Run a `CREATE TABLE` with an extra outer retry budget. The base
425
+ * `query()` already retries 3 times on fetch errors (~3.5s total), but a
426
+ * failed CREATE is permanent corruption — every subsequent SELECT against
427
+ * the missing table fails. Wrapping in an outer loop with longer backoff
428
+ * (2s, 5s, then 10s) gives us ~17s of reach across transient network
429
+ * blips before giving up. Failures still propagate; getApi() resets its
430
+ * cache on init failure (openclaw plugin) so the next call retries the
431
+ * whole init flow.
432
+ */
433
+ async createTableWithRetry(sql, label) {
434
+ const OUTER_BACKOFFS_MS = [2e3, 5e3, 1e4];
435
+ let lastErr = null;
436
+ for (let attempt = 0; attempt <= OUTER_BACKOFFS_MS.length; attempt++) {
437
+ try {
438
+ await this.query(sql);
439
+ return;
440
+ } catch (err) {
441
+ lastErr = err;
442
+ const msg = err instanceof Error ? err.message : String(err);
443
+ log2(`CREATE TABLE "${label}" attempt ${attempt + 1}/${OUTER_BACKOFFS_MS.length + 1} failed: ${msg}`);
444
+ if (attempt < OUTER_BACKOFFS_MS.length) {
445
+ await sleep(OUTER_BACKOFFS_MS[attempt]);
446
+ }
447
+ }
448
+ }
449
+ throw lastErr;
450
+ }
528
451
  /** Create the memory table if it doesn't already exist. Migrate columns on existing tables. */
529
452
  async ensureTable(name) {
530
453
  const tbl = name ?? this.tableName;
531
454
  const tables = await this.listTables();
532
455
  if (!tables.includes(tbl)) {
533
456
  log2(`table "${tbl}" not found, creating`);
534
- await this.query(
535
- `CREATE TABLE IF NOT EXISTS "${tbl}" (id TEXT NOT NULL DEFAULT '', path TEXT NOT NULL DEFAULT '', filename TEXT NOT NULL DEFAULT '', summary TEXT NOT NULL DEFAULT '', author TEXT NOT NULL DEFAULT '', mime_type TEXT NOT NULL DEFAULT 'text/plain', size_bytes BIGINT NOT NULL DEFAULT 0, project TEXT NOT NULL DEFAULT '', description TEXT NOT NULL DEFAULT '', agent TEXT NOT NULL DEFAULT '', creation_date TEXT NOT NULL DEFAULT '', last_update_date TEXT NOT NULL DEFAULT '') USING deeplake`
457
+ await this.createTableWithRetry(
458
+ `CREATE TABLE IF NOT EXISTS "${tbl}" (id TEXT NOT NULL DEFAULT '', path TEXT NOT NULL DEFAULT '', filename TEXT NOT NULL DEFAULT '', summary TEXT NOT NULL DEFAULT '', summary_embedding FLOAT4[], author TEXT NOT NULL DEFAULT '', mime_type TEXT NOT NULL DEFAULT 'text/plain', size_bytes BIGINT NOT NULL DEFAULT 0, project TEXT NOT NULL DEFAULT '', description TEXT NOT NULL DEFAULT '', agent TEXT NOT NULL DEFAULT '', creation_date TEXT NOT NULL DEFAULT '', last_update_date TEXT NOT NULL DEFAULT '') USING deeplake`,
459
+ tbl
536
460
  );
537
461
  log2(`table "${tbl}" created`);
538
462
  if (!tables.includes(tbl)) this._tablesCache = [...tables, tbl];
539
463
  }
464
+ await this.ensureEmbeddingColumn(tbl, SUMMARY_EMBEDDING_COL);
465
+ await this.ensureColumn(tbl, "agent", "TEXT NOT NULL DEFAULT ''");
540
466
  }
541
467
  /** Create the sessions table (uses JSONB for message since every row is a JSON event). */
542
468
  async ensureSessionsTable(name) {
543
469
  const tables = await this.listTables();
544
470
  if (!tables.includes(name)) {
545
471
  log2(`table "${name}" not found, creating`);
546
- await this.query(
547
- `CREATE TABLE IF NOT EXISTS "${name}" (id TEXT NOT NULL DEFAULT '', path TEXT NOT NULL DEFAULT '', filename TEXT NOT NULL DEFAULT '', message JSONB, author TEXT NOT NULL DEFAULT '', mime_type TEXT NOT NULL DEFAULT 'application/json', size_bytes BIGINT NOT NULL DEFAULT 0, project TEXT NOT NULL DEFAULT '', description TEXT NOT NULL DEFAULT '', agent TEXT NOT NULL DEFAULT '', creation_date TEXT NOT NULL DEFAULT '', last_update_date TEXT NOT NULL DEFAULT '') USING deeplake`
472
+ await this.createTableWithRetry(
473
+ `CREATE TABLE IF NOT EXISTS "${name}" (id TEXT NOT NULL DEFAULT '', path TEXT NOT NULL DEFAULT '', filename TEXT NOT NULL DEFAULT '', message JSONB, message_embedding FLOAT4[], author TEXT NOT NULL DEFAULT '', mime_type TEXT NOT NULL DEFAULT 'application/json', size_bytes BIGINT NOT NULL DEFAULT 0, project TEXT NOT NULL DEFAULT '', description TEXT NOT NULL DEFAULT '', agent TEXT NOT NULL DEFAULT '', creation_date TEXT NOT NULL DEFAULT '', last_update_date TEXT NOT NULL DEFAULT '') USING deeplake`,
474
+ name
548
475
  );
549
476
  log2(`table "${name}" created`);
550
477
  if (!tables.includes(name)) this._tablesCache = [...tables, name];
551
478
  }
479
+ await this.ensureEmbeddingColumn(name, MESSAGE_EMBEDDING_COL);
480
+ await this.ensureColumn(name, "agent", "TEXT NOT NULL DEFAULT ''");
552
481
  await this.ensureLookupIndex(name, "path_creation_date", `("path", "creation_date")`);
553
482
  }
554
483
  };
@@ -700,22 +629,25 @@ function normalizeContent(path, raw) {
700
629
  return raw;
701
630
  }
702
631
  if (Array.isArray(obj.turns)) {
703
- const header = [];
704
- if (obj.date_time) header.push(`date: ${obj.date_time}`);
705
- if (obj.speakers) {
706
- const s = obj.speakers;
707
- const names = [s.speaker_a, s.speaker_b].filter(Boolean).join(", ");
708
- if (names) header.push(`speakers: ${names}`);
709
- }
632
+ const dateHeader = obj.date_time ? `(${String(obj.date_time)}) ` : "";
710
633
  const lines = obj.turns.map((t) => {
711
634
  const sp = String(t?.speaker ?? t?.name ?? "?").trim();
712
635
  const tx = String(t?.text ?? t?.content ?? "").replace(/\s+/g, " ").trim();
713
636
  const tag = t?.dia_id ? `[${t.dia_id}] ` : "";
714
- return `${tag}${sp}: ${tx}`;
637
+ return `${dateHeader}${tag}${sp}: ${tx}`;
715
638
  });
716
- const out2 = [...header, ...lines].join("\n");
639
+ const out2 = lines.join("\n");
717
640
  return out2.trim() ? out2 : raw;
718
641
  }
642
+ if (obj.turn && typeof obj.turn === "object" && !Array.isArray(obj.turn)) {
643
+ const t = obj.turn;
644
+ const sp = String(t.speaker ?? t.name ?? "?").trim();
645
+ const tx = String(t.text ?? t.content ?? "").replace(/\s+/g, " ").trim();
646
+ const tag = t.dia_id ? `[${String(t.dia_id)}] ` : "";
647
+ const dateHeader = obj.date_time ? `(${String(obj.date_time)}) ` : "";
648
+ const line = `${dateHeader}${tag}${sp}: ${tx}`;
649
+ return line.trim() ? line : raw;
650
+ }
719
651
  const stripRecalled = (t) => {
720
652
  const i = t.indexOf("<recalled-memories>");
721
653
  if (i === -1) return t;
@@ -753,8 +685,43 @@ function buildPathCondition(targetPath) {
753
685
  return `(path = '${sqlStr(clean)}' OR path LIKE '${sqlLike(clean)}/%' ESCAPE '\\')`;
754
686
  }
755
687
  async function searchDeeplakeTables(api2, memoryTable2, sessionsTable2, opts) {
756
- const { pathFilter, contentScanOnly, likeOp, escapedPattern, prefilterPattern, prefilterPatterns, multiWordPatterns } = opts;
688
+ const { pathFilter, contentScanOnly, likeOp, escapedPattern, prefilterPattern, prefilterPatterns, queryEmbedding, multiWordPatterns } = opts;
757
689
  const limit = opts.limit ?? 100;
690
+ if (queryEmbedding && queryEmbedding.length > 0) {
691
+ const vecLit = serializeFloat4Array(queryEmbedding);
692
+ const semanticLimit = Math.min(
693
+ limit,
694
+ Number(process.env.HIVEMIND_SEMANTIC_LIMIT ?? "20")
695
+ );
696
+ const lexicalLimit = Math.min(
697
+ limit,
698
+ Number(process.env.HIVEMIND_HYBRID_LEXICAL_LIMIT ?? "20")
699
+ );
700
+ const filterPatternsForLex = contentScanOnly ? prefilterPatterns && prefilterPatterns.length > 0 ? prefilterPatterns : prefilterPattern ? [prefilterPattern] : [] : [escapedPattern];
701
+ const memLexFilter = buildContentFilter("summary::text", likeOp, filterPatternsForLex);
702
+ const sessLexFilter = buildContentFilter("message::text", likeOp, filterPatternsForLex);
703
+ const memLexQuery = memLexFilter ? `SELECT path, summary::text AS content, 0 AS source_order, '' AS creation_date, 1.0 AS score FROM "${memoryTable2}" WHERE 1=1${pathFilter}${memLexFilter} LIMIT ${lexicalLimit}` : null;
704
+ const sessLexQuery = sessLexFilter ? `SELECT path, message::text AS content, 1 AS source_order, COALESCE(creation_date::text, '') AS creation_date, 1.0 AS score FROM "${sessionsTable2}" WHERE 1=1${pathFilter}${sessLexFilter} LIMIT ${lexicalLimit}` : null;
705
+ const memSemQuery = `SELECT path, summary::text AS content, 0 AS source_order, '' AS creation_date, (summary_embedding <#> ${vecLit}) AS score FROM "${memoryTable2}" WHERE ARRAY_LENGTH(summary_embedding, 1) > 0${pathFilter} ORDER BY score DESC LIMIT ${semanticLimit}`;
706
+ const sessSemQuery = `SELECT path, message::text AS content, 1 AS source_order, COALESCE(creation_date::text, '') AS creation_date, (message_embedding <#> ${vecLit}) AS score FROM "${sessionsTable2}" WHERE ARRAY_LENGTH(message_embedding, 1) > 0${pathFilter} ORDER BY score DESC LIMIT ${semanticLimit}`;
707
+ const parts = [memSemQuery, sessSemQuery];
708
+ if (memLexQuery) parts.push(memLexQuery);
709
+ if (sessLexQuery) parts.push(sessLexQuery);
710
+ const unionSql = parts.map((q) => `(${q})`).join(" UNION ALL ");
711
+ const outerLimit = semanticLimit + lexicalLimit;
712
+ const rows2 = await api2.query(
713
+ `SELECT path, content, source_order, creation_date, score FROM (` + unionSql + `) AS combined ORDER BY score DESC LIMIT ${outerLimit}`
714
+ );
715
+ const seen = /* @__PURE__ */ new Set();
716
+ const unique = [];
717
+ for (const row of rows2) {
718
+ const p = String(row["path"]);
719
+ if (seen.has(p)) continue;
720
+ seen.add(p);
721
+ unique.push({ path: p, content: String(row["content"] ?? "") });
722
+ }
723
+ return unique;
724
+ }
758
725
  const filterPatterns = contentScanOnly ? prefilterPatterns && prefilterPatterns.length > 0 ? prefilterPatterns : prefilterPattern ? [prefilterPattern] : [] : multiWordPatterns && multiWordPatterns.length > 1 ? multiWordPatterns : [escapedPattern];
759
726
  const memFilter = buildContentFilter("summary::text", likeOp, filterPatterns);
760
727
  const sessFilter = buildContentFilter("message::text", likeOp, filterPatterns);
@@ -768,6 +735,14 @@ async function searchDeeplakeTables(api2, memoryTable2, sessionsTable2, opts) {
768
735
  content: String(row["content"] ?? "")
769
736
  }));
770
737
  }
738
+ function serializeFloat4Array(vec) {
739
+ const parts = [];
740
+ for (const v of vec) {
741
+ if (!Number.isFinite(v)) return "NULL";
742
+ parts.push(String(v));
743
+ }
744
+ return `ARRAY[${parts.join(",")}]::float4[]`;
745
+ }
771
746
  function buildPathFilter(targetPath) {
772
747
  const condition = buildPathCondition(targetPath);
773
748
  return condition ? ` AND ${condition}` : "";
@@ -842,7 +817,7 @@ function buildGrepSearchOptions(params, targetPath) {
842
817
  return {
843
818
  pathFilter: buildPathFilter(targetPath),
844
819
  contentScanOnly: hasRegexMeta,
845
- likeOp: params.ignoreCase ? "ILIKE" : "LIKE",
820
+ likeOp: process.env.HIVEMIND_GREP_LIKE === "case-sensitive" ? "LIKE" : "ILIKE",
846
821
  escapedPattern: sqlLike(params.pattern),
847
822
  prefilterPattern: literalPrefilter ? sqlLike(literalPrefilter) : void 0,
848
823
  prefilterPatterns: alternationPrefilters?.map((literal) => sqlLike(literal)),
@@ -871,33 +846,64 @@ function compileGrepRegex(params) {
871
846
  function normalizeSessionPart(path, content) {
872
847
  return normalizeContent(path, content);
873
848
  }
874
- function buildVirtualIndexContent(summaryRows, sessionRows = []) {
875
- const total = summaryRows.length + sessionRows.length;
849
+ var INDEX_LIMIT_PER_SECTION = 50;
850
+ function buildVirtualIndexContent(summaryRows, sessionRows = [], opts = {}) {
876
851
  const lines = [
877
- "# Memory Index",
852
+ "# Session Index",
878
853
  "",
879
- `${total} entries (${summaryRows.length} summaries, ${sessionRows.length} sessions):`,
854
+ "Two sources are available. Consult the section relevant to the question.",
880
855
  ""
881
856
  ];
882
- if (summaryRows.length > 0) {
883
- lines.push("## Summaries", "");
857
+ lines.push("## memory", "");
858
+ if (summaryRows.length === 0) {
859
+ lines.push("_(empty \u2014 no summaries ingested yet)_");
860
+ } else {
861
+ lines.push("AI-generated summaries per session. Read these first for topic-level overviews.");
862
+ lines.push("");
863
+ if (opts.summaryTruncated) {
864
+ lines.push(`_Showing ${INDEX_LIMIT_PER_SECTION} most-recent of many \u2014 older summaries reachable via \`Grep pattern="..." path="~/.deeplake/memory"\`._`);
865
+ lines.push("");
866
+ }
867
+ lines.push("| Session | Created | Last Updated | Project | Description |");
868
+ lines.push("|---------|---------|--------------|---------|-------------|");
884
869
  for (const row of summaryRows) {
885
- const path = row["path"];
870
+ const p = row["path"] || "";
871
+ const match = p.match(/\/summaries\/([^/]+)\/([^/]+)\.md$/);
872
+ if (!match) continue;
873
+ const summaryUser = match[1];
874
+ const sessionId = match[2];
875
+ const relPath = `summaries/${summaryUser}/${sessionId}.md`;
886
876
  const project = row["project"] || "";
887
- const description = (row["description"] || "").slice(0, 120);
888
- const date = (row["creation_date"] || "").slice(0, 10);
889
- lines.push(`- [${path}](${path}) ${date} ${project ? `[${project}]` : ""} ${description}`);
877
+ const description = row["description"] || "";
878
+ const creationDate = row["creation_date"] || "";
879
+ const lastUpdateDate = row["last_update_date"] || "";
880
+ lines.push(`| [${sessionId}](${relPath}) | ${creationDate} | ${lastUpdateDate} | ${project} | ${description} |`);
890
881
  }
891
- lines.push("");
892
882
  }
893
- if (sessionRows.length > 0) {
894
- lines.push("## Sessions", "");
883
+ lines.push("");
884
+ lines.push("## sessions", "");
885
+ if (sessionRows.length === 0) {
886
+ lines.push("_(empty \u2014 no session records ingested yet)_");
887
+ } else {
888
+ lines.push("Raw session records (dialogue, tool calls). Read for exact detail / quotes.");
889
+ lines.push("");
890
+ if (opts.sessionTruncated) {
891
+ lines.push(`_Showing ${INDEX_LIMIT_PER_SECTION} most-recent of many \u2014 older sessions reachable via \`Grep pattern="..." path="~/.deeplake/memory"\`._`);
892
+ lines.push("");
893
+ }
894
+ lines.push("| Session | Created | Last Updated | Description |");
895
+ lines.push("|---------|---------|--------------|-------------|");
895
896
  for (const row of sessionRows) {
896
- const path = row["path"];
897
- const description = (row["description"] || "").slice(0, 120);
898
- lines.push(`- [${path}](${path}) ${description}`);
897
+ const p = row["path"] || "";
898
+ const rel = p.startsWith("/") ? p.slice(1) : p;
899
+ const filename = p.split("/").pop() ?? p;
900
+ const description = row["description"] || "";
901
+ const creationDate = row["creation_date"] || "";
902
+ const lastUpdateDate = row["last_update_date"] || "";
903
+ lines.push(`| [${filename}](${rel}) | ${creationDate} | ${lastUpdateDate} | ${description} |`);
899
904
  }
900
905
  }
906
+ lines.push("");
901
907
  return lines.join("\n");
902
908
  }
903
909
  function buildUnionQuery(memoryQuery, sessionsQuery) {
@@ -954,15 +960,25 @@ async function readVirtualPathContents(api2, memoryTable2, sessionsTable2, virtu
954
960
  }
955
961
  }
956
962
  if (result.get("/index.md") === null && uniquePaths.includes("/index.md")) {
963
+ const fetchLimit = INDEX_LIMIT_PER_SECTION + 1;
957
964
  const [summaryRows, sessionRows] = await Promise.all([
958
965
  api2.query(
959
- `SELECT path, project, description, creation_date FROM "${memoryTable2}" WHERE path LIKE '/summaries/%' ORDER BY creation_date DESC`
966
+ `SELECT path, project, description, creation_date, last_update_date FROM "${memoryTable2}" WHERE path LIKE '/summaries/%' ORDER BY last_update_date DESC LIMIT ${fetchLimit}`
960
967
  ).catch(() => []),
961
968
  api2.query(
962
- `SELECT path, description FROM "${sessionsTable2}" WHERE path LIKE '/sessions/%' ORDER BY path`
969
+ `SELECT path, MAX(description) AS description, MIN(creation_date) AS creation_date, MAX(last_update_date) AS last_update_date FROM "${sessionsTable2}" WHERE path LIKE '/sessions/%' GROUP BY path ORDER BY MAX(last_update_date) DESC LIMIT ${fetchLimit}`
963
970
  ).catch(() => [])
964
971
  ]);
965
- result.set("/index.md", buildVirtualIndexContent(summaryRows, sessionRows));
972
+ const summaryTruncated = summaryRows.length > INDEX_LIMIT_PER_SECTION;
973
+ const sessionTruncated = sessionRows.length > INDEX_LIMIT_PER_SECTION;
974
+ result.set(
975
+ "/index.md",
976
+ buildVirtualIndexContent(
977
+ summaryRows.slice(0, INDEX_LIMIT_PER_SECTION),
978
+ sessionRows.slice(0, INDEX_LIMIT_PER_SECTION),
979
+ { summaryTruncated, sessionTruncated }
980
+ )
981
+ );
966
982
  }
967
983
  return result;
968
984
  }
@@ -974,6 +990,32 @@ async function readVirtualPathContent(api2, memoryTable2, sessionsTable2, virtua
974
990
  function definePluginEntry(entry) {
975
991
  return entry;
976
992
  }
993
+ function loadSetupConfig() {
994
+ return import("./chunks/setup-config-C35UK4LP.js");
995
+ }
996
+ var credsModulePromise = null;
997
+ var configModulePromise = null;
998
+ function loadCredsModule() {
999
+ if (!credsModulePromise) credsModulePromise = import("./chunks/auth-creds-AEKS6D3P.js");
1000
+ return credsModulePromise;
1001
+ }
1002
+ function loadConfigModule() {
1003
+ if (!configModulePromise) configModulePromise = import("./chunks/config-G23NI5TV.js");
1004
+ return configModulePromise;
1005
+ }
1006
+ async function loadCredentials2() {
1007
+ const m = await loadCredsModule();
1008
+ return m.loadCredentials();
1009
+ }
1010
+ async function saveCredentials2(creds) {
1011
+ if (!creds) return;
1012
+ const m = await loadCredsModule();
1013
+ m.saveCredentials(creds);
1014
+ }
1015
+ async function loadConfig() {
1016
+ const m = await loadConfigModule();
1017
+ return m.loadConfig();
1018
+ }
977
1019
  var DEFAULT_API_URL2 = "https://api.deeplake.ai";
978
1020
  var VERSION_URL = "https://clawhub.ai/api/v1/packages/hivemind";
979
1021
  function extractLatestVersion(body) {
@@ -984,7 +1026,7 @@ function extractLatestVersion(body) {
984
1026
  return typeof v === "string" && v.length > 0 ? v : null;
985
1027
  }
986
1028
  function getInstalledVersion() {
987
- return "0.6.47".length > 0 ? "0.6.47" : null;
1029
+ return "0.7.4".length > 0 ? "0.7.4" : null;
988
1030
  }
989
1031
  function isNewer(latest, current) {
990
1032
  const parse = (v) => v.replace(/-.*$/, "").split(".").map(Number);
@@ -1048,7 +1090,8 @@ async function requestAuth() {
1048
1090
  headers: {
1049
1091
  Authorization: `Bearer ${token}`,
1050
1092
  "Content-Type": "application/json",
1051
- "X-Activeloop-Org-Id": orgId
1093
+ "X-Activeloop-Org-Id": orgId,
1094
+ ...deeplakeClientHeader()
1052
1095
  },
1053
1096
  body: JSON.stringify({ name: `hivemind-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`, duration: 365 * 24 * 60 * 60, organization_id: orgId })
1054
1097
  });
@@ -1059,7 +1102,7 @@ async function requestAuth() {
1059
1102
  } catch {
1060
1103
  }
1061
1104
  }
1062
- saveCredentials({ token: savedToken, orgId, orgName, userName, apiUrl: DEFAULT_API_URL2, savedAt: (/* @__PURE__ */ new Date()).toISOString() });
1105
+ await saveCredentials2({ token: savedToken, orgId, orgName, userName, apiUrl: DEFAULT_API_URL2, savedAt: (/* @__PURE__ */ new Date()).toISOString() });
1063
1106
  authPending = false;
1064
1107
  authUrl = null;
1065
1108
  justAuthenticated = true;
@@ -1150,16 +1193,17 @@ function normalizeVirtualPath(p) {
1150
1193
  }
1151
1194
  async function getApi() {
1152
1195
  if (api) return api;
1153
- const config = loadConfig();
1196
+ const config = await loadConfig();
1154
1197
  if (!config) {
1155
1198
  if (!authPending) await requestAuth();
1156
1199
  return null;
1157
1200
  }
1158
1201
  sessionsTable = config.sessionsTableName;
1159
1202
  memoryTable = config.tableName;
1160
- api = new DeeplakeApi(config.token, config.apiUrl, config.orgId, config.workspaceId, config.tableName);
1161
- await api.ensureTable();
1162
- await api.ensureSessionsTable(sessionsTable);
1203
+ const candidate = new DeeplakeApi(config.token, config.apiUrl, config.orgId, config.workspaceId, config.tableName);
1204
+ await candidate.ensureTable();
1205
+ await candidate.ensureSessionsTable(sessionsTable);
1206
+ api = candidate;
1163
1207
  return api;
1164
1208
  }
1165
1209
  var src_default = definePluginEntry({
@@ -1167,432 +1211,437 @@ var src_default = definePluginEntry({
1167
1211
  name: "Hivemind",
1168
1212
  description: "Cloud-backed shared memory powered by Deeplake",
1169
1213
  register(pluginApi) {
1170
- try {
1171
- pluginApi.registerCommand({
1172
- name: "hivemind_login",
1173
- description: "Log in to Hivemind (or switch accounts)",
1174
- handler: async () => {
1175
- const existing = loadCredentials();
1176
- const url = await requestAuth();
1177
- if (existing?.token) {
1178
- return {
1179
- text: `\u2139\uFE0F Currently logged in as ${existing.orgName ?? existing.orgId}.
1214
+ void (async () => {
1215
+ try {
1216
+ pluginApi.registerCommand({
1217
+ name: "hivemind_login",
1218
+ description: "Log in to Hivemind (or switch accounts)",
1219
+ handler: async () => {
1220
+ const existing = await loadCredentials2();
1221
+ const url = await requestAuth();
1222
+ if (existing?.token) {
1223
+ return {
1224
+ text: `\u2139\uFE0F Currently logged in as ${existing.orgName ?? existing.orgId}.
1180
1225
 
1181
1226
  To re-authenticate or switch accounts:
1182
1227
 
1183
1228
  ${url}
1184
1229
 
1185
1230
  After signing in, send another message.`
1186
- };
1187
- }
1188
- return { text: `\u{1F510} Sign in to activate Hivemind memory:
1231
+ };
1232
+ }
1233
+ return { text: `\u{1F510} Sign in to activate Hivemind memory:
1189
1234
 
1190
1235
  ${url}
1191
1236
 
1192
1237
  After signing in, send another message.` };
1193
- }
1194
- });
1195
- pluginApi.registerCommand({
1196
- name: "hivemind_capture",
1197
- description: "Toggle conversation capture on/off",
1198
- handler: async () => {
1199
- captureEnabled = !captureEnabled;
1200
- return { text: captureEnabled ? "\u2705 Capture enabled \u2014 conversations will be stored to Hivemind." : "\u23F8\uFE0F Capture paused \u2014 conversations will NOT be stored until you run /hivemind_capture again." };
1201
- }
1202
- });
1203
- pluginApi.registerCommand({
1204
- name: "hivemind_whoami",
1205
- description: "Show current Hivemind org and workspace",
1206
- handler: async () => {
1207
- const creds2 = loadCredentials();
1208
- if (!creds2?.token) return { text: "Not logged in. Run /hivemind_login" };
1209
- return { text: `Org: ${creds2.orgName ?? creds2.orgId}
1238
+ }
1239
+ });
1240
+ pluginApi.registerCommand({
1241
+ name: "hivemind_capture",
1242
+ description: "Toggle conversation capture on/off",
1243
+ handler: async () => {
1244
+ captureEnabled = !captureEnabled;
1245
+ return { text: captureEnabled ? "\u2705 Capture enabled \u2014 conversations will be stored to Hivemind." : "\u23F8\uFE0F Capture paused \u2014 conversations will NOT be stored until you run /hivemind_capture again." };
1246
+ }
1247
+ });
1248
+ pluginApi.registerCommand({
1249
+ name: "hivemind_whoami",
1250
+ description: "Show current Hivemind org and workspace",
1251
+ handler: async () => {
1252
+ const creds2 = await loadCredentials2();
1253
+ if (!creds2?.token) return { text: "Not logged in. Run /hivemind_login" };
1254
+ return { text: `Org: ${creds2.orgName ?? creds2.orgId}
1210
1255
  Workspace: ${creds2.workspaceId ?? "default"}` };
1211
- }
1212
- });
1213
- pluginApi.registerCommand({
1214
- name: "hivemind_orgs",
1215
- description: "List available organizations",
1216
- handler: async () => {
1217
- const creds2 = loadCredentials();
1218
- if (!creds2?.token) return { text: "Not logged in. Run /hivemind_login" };
1219
- const orgs = await listOrgs(creds2.token, creds2.apiUrl);
1220
- if (!orgs.length) return { text: "No organizations found." };
1221
- const lines = orgs.map((o) => `${o.id === creds2.orgId ? "\u2192 " : " "}${o.name}`);
1222
- return { text: lines.join("\n") };
1223
- }
1224
- });
1225
- pluginApi.registerCommand({
1226
- name: "hivemind_switch_org",
1227
- description: "Switch to a different organization",
1228
- acceptsArgs: true,
1229
- handler: async (ctx) => {
1230
- const creds2 = loadCredentials();
1231
- if (!creds2?.token) return { text: "Not logged in. Run /hivemind_login" };
1232
- const target = ctx.args?.trim();
1233
- if (!target) return { text: "Usage: /hivemind_switch_org <name-or-id>" };
1234
- const orgs = await listOrgs(creds2.token, creds2.apiUrl);
1235
- const lc = target.toLowerCase();
1236
- const match = orgs.find((o) => o.id === target || o.name.toLowerCase() === lc) ?? orgs.find((o) => o.name.toLowerCase().includes(lc) || o.id.toLowerCase().includes(lc));
1237
- if (!match) {
1238
- const available = orgs.length ? orgs.map((o) => ` - ${o.name} (id: ${o.id})`).join("\n") : " (none \u2014 your current token has no organization access)";
1239
- return { text: `Org not found: ${target}
1256
+ }
1257
+ });
1258
+ pluginApi.registerCommand({
1259
+ name: "hivemind_orgs",
1260
+ description: "List available organizations",
1261
+ handler: async () => {
1262
+ const creds2 = await loadCredentials2();
1263
+ if (!creds2?.token) return { text: "Not logged in. Run /hivemind_login" };
1264
+ const orgs = await listOrgs(creds2.token, creds2.apiUrl);
1265
+ if (!orgs.length) return { text: "No organizations found." };
1266
+ const lines = orgs.map((o) => `${o.id === creds2.orgId ? "\u2192 " : " "}${o.name}`);
1267
+ return { text: lines.join("\n") };
1268
+ }
1269
+ });
1270
+ pluginApi.registerCommand({
1271
+ name: "hivemind_switch_org",
1272
+ description: "Switch to a different organization",
1273
+ acceptsArgs: true,
1274
+ handler: async (ctx) => {
1275
+ const creds2 = await loadCredentials2();
1276
+ if (!creds2?.token) return { text: "Not logged in. Run /hivemind_login" };
1277
+ const target = ctx.args?.trim();
1278
+ if (!target) return { text: "Usage: /hivemind_switch_org <name-or-id>" };
1279
+ const orgs = await listOrgs(creds2.token, creds2.apiUrl);
1280
+ const lc = target.toLowerCase();
1281
+ const match = orgs.find((o) => o.id === target || o.name.toLowerCase() === lc) ?? orgs.find((o) => o.name.toLowerCase().includes(lc) || o.id.toLowerCase().includes(lc));
1282
+ if (!match) {
1283
+ const available = orgs.length ? orgs.map((o) => ` - ${o.name} (id: ${o.id})`).join("\n") : " (none \u2014 your current token has no organization access)";
1284
+ return { text: `Org not found: ${target}
1240
1285
 
1241
1286
  Available:
1242
1287
  ${available}` };
1288
+ }
1289
+ await switchOrg(match.id, match.name);
1290
+ api = null;
1291
+ return { text: `Switched to org: ${match.name}` };
1243
1292
  }
1244
- await switchOrg(match.id, match.name);
1245
- api = null;
1246
- return { text: `Switched to org: ${match.name}` };
1247
- }
1248
- });
1249
- pluginApi.registerCommand({
1250
- name: "hivemind_workspaces",
1251
- description: "List available workspaces",
1252
- handler: async () => {
1253
- const creds2 = loadCredentials();
1254
- if (!creds2?.token) return { text: "Not logged in. Run /hivemind_login" };
1255
- const ws = await listWorkspaces(creds2.token, creds2.apiUrl, creds2.orgId);
1256
- if (!ws.length) return { text: "No workspaces found." };
1257
- const lines = ws.map((w) => `${w.id === (creds2.workspaceId ?? "default") ? "\u2192 " : " "}${w.name}`);
1258
- return { text: lines.join("\n") };
1259
- }
1260
- });
1261
- pluginApi.registerCommand({
1262
- name: "hivemind_switch_workspace",
1263
- description: "Switch to a different workspace",
1264
- acceptsArgs: true,
1265
- handler: async (ctx) => {
1266
- const creds2 = loadCredentials();
1267
- if (!creds2?.token) return { text: "Not logged in. Run /hivemind_login" };
1268
- const target = ctx.args?.trim();
1269
- if (!target) return { text: "Usage: /hivemind_switch_workspace <name-or-id>" };
1270
- const ws = await listWorkspaces(creds2.token, creds2.apiUrl, creds2.orgId);
1271
- const lc = target.toLowerCase();
1272
- const match = ws.find((w) => w.id === target || w.name.toLowerCase() === lc) ?? ws.find((w) => w.name.toLowerCase().includes(lc) || w.id.toLowerCase().includes(lc));
1273
- if (!match) {
1274
- const available = ws.length ? ws.map((w) => ` - ${w.name} (id: ${w.id})`).join("\n") : " (none in current org \u2014 try /hivemind_switch_org first)";
1275
- return { text: `Workspace not found: ${target}
1293
+ });
1294
+ pluginApi.registerCommand({
1295
+ name: "hivemind_workspaces",
1296
+ description: "List available workspaces",
1297
+ handler: async () => {
1298
+ const creds2 = await loadCredentials2();
1299
+ if (!creds2?.token) return { text: "Not logged in. Run /hivemind_login" };
1300
+ const ws = await listWorkspaces(creds2.token, creds2.apiUrl, creds2.orgId);
1301
+ if (!ws.length) return { text: "No workspaces found." };
1302
+ const lines = ws.map((w) => `${w.id === (creds2.workspaceId ?? "default") ? "\u2192 " : " "}${w.name}`);
1303
+ return { text: lines.join("\n") };
1304
+ }
1305
+ });
1306
+ pluginApi.registerCommand({
1307
+ name: "hivemind_switch_workspace",
1308
+ description: "Switch to a different workspace",
1309
+ acceptsArgs: true,
1310
+ handler: async (ctx) => {
1311
+ const creds2 = await loadCredentials2();
1312
+ if (!creds2?.token) return { text: "Not logged in. Run /hivemind_login" };
1313
+ const target = ctx.args?.trim();
1314
+ if (!target) return { text: "Usage: /hivemind_switch_workspace <name-or-id>" };
1315
+ const ws = await listWorkspaces(creds2.token, creds2.apiUrl, creds2.orgId);
1316
+ const lc = target.toLowerCase();
1317
+ const match = ws.find((w) => w.id === target || w.name.toLowerCase() === lc) ?? ws.find((w) => w.name.toLowerCase().includes(lc) || w.id.toLowerCase().includes(lc));
1318
+ if (!match) {
1319
+ const available = ws.length ? ws.map((w) => ` - ${w.name} (id: ${w.id})`).join("\n") : " (none in current org \u2014 try /hivemind_switch_org first)";
1320
+ return { text: `Workspace not found: ${target}
1276
1321
 
1277
1322
  Available:
1278
1323
  ${available}` };
1324
+ }
1325
+ await switchWorkspace(match.id);
1326
+ api = null;
1327
+ return { text: `Switched to workspace: ${match.name}` };
1279
1328
  }
1280
- await switchWorkspace(match.id);
1281
- api = null;
1282
- return { text: `Switched to workspace: ${match.name}` };
1283
- }
1284
- });
1285
- pluginApi.registerCommand({
1286
- name: "hivemind_setup",
1287
- description: "Add Hivemind tools to your openclaw allowlist (needed once per install)",
1288
- handler: async () => {
1289
- const result = ensureHivemindAllowlisted();
1290
- if (result.status === "already-set") {
1291
- return { text: `\u2705 Hivemind tools are already enabled in your allowlist.
1329
+ });
1330
+ pluginApi.registerCommand({
1331
+ name: "hivemind_setup",
1332
+ description: "Add Hivemind tools to your openclaw allowlist (needed once per install)",
1333
+ handler: async () => {
1334
+ const { ensureHivemindAllowlisted } = await loadSetupConfig();
1335
+ const result = ensureHivemindAllowlisted();
1336
+ if (result.status === "already-set") {
1337
+ return { text: `\u2705 Hivemind tools are already enabled in your allowlist.
1292
1338
 
1293
1339
  No changes needed \u2014 memory tools are available to the agent.` };
1294
- }
1295
- if (result.status === "added") {
1296
- return { text: `\u2705 Added "hivemind" to your tool allowlist.
1340
+ }
1341
+ if (result.status === "added") {
1342
+ return { text: `\u2705 Added "hivemind" to your tool allowlist.
1297
1343
 
1298
1344
  Openclaw will detect the config change and restart. On the next turn, the agent will have access to hivemind_search, hivemind_read, and hivemind_index.
1299
1345
 
1300
1346
  Backup of previous config: ${result.backupPath}` };
1301
- }
1302
- return { text: `\u26A0\uFE0F Could not update allowlist: ${result.error}
1347
+ }
1348
+ return { text: `\u26A0\uFE0F Could not update allowlist: ${result.error}
1303
1349
 
1304
1350
  Manual fix: open ${result.configPath} and add "hivemind" to the "alsoAllow" array under "tools".` };
1305
- }
1306
- });
1307
- pluginApi.registerCommand({
1308
- name: "hivemind_version",
1309
- description: "Show the installed Hivemind version and check for updates",
1310
- handler: async () => {
1311
- const current = getInstalledVersion();
1312
- if (!current) return { text: "Could not determine installed version." };
1313
- try {
1314
- const res = await fetch(VERSION_URL, { signal: AbortSignal.timeout(3e3) });
1315
- if (!res.ok) return { text: `Current version: ${current}. Could not check for updates.` };
1316
- const latest = extractLatestVersion(await res.json());
1317
- if (!latest) return { text: `Current version: ${current}. Could not parse latest version.` };
1318
- if (isNewer(latest, current)) {
1319
- return { text: `\u2B06\uFE0F Update available: ${current} \u2192 ${latest}
1351
+ }
1352
+ });
1353
+ pluginApi.registerCommand({
1354
+ name: "hivemind_version",
1355
+ description: "Show the installed Hivemind version and check for updates",
1356
+ handler: async () => {
1357
+ const current = getInstalledVersion();
1358
+ if (!current) return { text: "Could not determine installed version." };
1359
+ try {
1360
+ const res = await fetch(VERSION_URL, { signal: AbortSignal.timeout(3e3) });
1361
+ if (!res.ok) return { text: `Current version: ${current}. Could not check for updates.` };
1362
+ const latest = extractLatestVersion(await res.json());
1363
+ if (!latest) return { text: `Current version: ${current}. Could not parse latest version.` };
1364
+ if (isNewer(latest, current)) {
1365
+ return { text: `\u2B06\uFE0F Update available: ${current} \u2192 ${latest}
1320
1366
 
1321
1367
  Run /hivemind_update to install it now.` };
1368
+ }
1369
+ return { text: `\u2705 Hivemind v${current} is up to date.` };
1370
+ } catch {
1371
+ return { text: `Current version: ${current}. Could not check for updates.` };
1322
1372
  }
1323
- return { text: `\u2705 Hivemind v${current} is up to date.` };
1324
- } catch {
1325
- return { text: `Current version: ${current}. Could not check for updates.` };
1326
1373
  }
1327
- }
1328
- });
1329
- pluginApi.registerCommand({
1330
- name: "hivemind_update",
1331
- description: "Install the latest Hivemind version from ClawHub",
1332
- handler: async () => {
1333
- const current = getInstalledVersion() ?? "unknown";
1334
- return {
1335
- text: `Hivemind v${current} installed. To install the latest:
1374
+ });
1375
+ pluginApi.registerCommand({
1376
+ name: "hivemind_update",
1377
+ description: "Install the latest Hivemind version from ClawHub",
1378
+ handler: async () => {
1379
+ const current = getInstalledVersion() ?? "unknown";
1380
+ return {
1381
+ text: `Hivemind v${current} installed. To install the latest:
1336
1382
 
1337
1383
  \u2022 Ask me in chat: "update hivemind" \u2014 I'll run \`openclaw plugins update hivemind\` via my exec tool.
1338
1384
  \u2022 Or run in your terminal: \`openclaw plugins update hivemind\`
1339
1385
 
1340
1386
  The gateway restarts automatically once the install completes.`
1341
- };
1342
- }
1343
- });
1344
- pluginApi.registerCommand({
1345
- name: "hivemind_autoupdate",
1346
- description: "Toggle Hivemind auto-update on/off",
1347
- acceptsArgs: true,
1348
- handler: async (ctx) => {
1349
- const arg = ctx.args?.trim().toLowerCase();
1350
- let setTo;
1351
- if (arg === "on" || arg === "true" || arg === "enable") setTo = true;
1352
- else if (arg === "off" || arg === "false" || arg === "disable") setTo = false;
1353
- const result = toggleAutoUpdateConfig(setTo);
1354
- if (result.status === "error") {
1355
- return { text: `\u26A0\uFE0F Could not update auto-update setting: ${result.error}` };
1387
+ };
1356
1388
  }
1357
- return {
1358
- text: result.newValue ? "\u2705 Auto-update is ON. Hivemind will install new versions automatically when the gateway starts." : "\u23F8\uFE0F Auto-update is OFF. Run /hivemind_update manually to install new versions."
1359
- };
1360
- }
1361
- });
1362
- pluginApi.registerTool({
1363
- name: "hivemind_search",
1364
- label: "Hivemind Search",
1365
- description: "Search Hivemind shared memory (summaries + past session turns) for keywords, phrases, or regex. Returns matching path + snippet pairs from BOTH the memory and sessions tables. Use this FIRST when the user asks about past work, decisions, people, or anything that might live in memory.",
1366
- parameters: {
1367
- type: "object",
1368
- additionalProperties: false,
1369
- properties: {
1370
- query: {
1371
- type: "string",
1372
- minLength: 1,
1373
- description: "Search text. Treated as a literal substring by default; set `regex: true` to use regex metacharacters."
1374
- },
1375
- path: {
1376
- type: "string",
1377
- description: "Optional virtual path prefix to scope the search, e.g. '/summaries/' or '/sessions/alice/'. Defaults to '/' (all of memory)."
1378
- },
1379
- regex: {
1380
- type: "boolean",
1381
- description: "If true, `query` is interpreted as a regex. Default false (literal substring)."
1382
- },
1383
- ignoreCase: {
1384
- type: "boolean",
1385
- description: "Case-insensitive match. Default true."
1386
- },
1387
- limit: {
1388
- type: "integer",
1389
- minimum: 1,
1390
- maximum: 100,
1391
- description: "Max rows returned per table. Default 20."
1389
+ });
1390
+ pluginApi.registerCommand({
1391
+ name: "hivemind_autoupdate",
1392
+ description: "Toggle Hivemind auto-update on/off",
1393
+ acceptsArgs: true,
1394
+ handler: async (ctx) => {
1395
+ const arg = ctx.args?.trim().toLowerCase();
1396
+ let setTo;
1397
+ if (arg === "on" || arg === "true" || arg === "enable") setTo = true;
1398
+ else if (arg === "off" || arg === "false" || arg === "disable") setTo = false;
1399
+ const { toggleAutoUpdateConfig } = await loadSetupConfig();
1400
+ const result = toggleAutoUpdateConfig(setTo);
1401
+ if (result.status === "error") {
1402
+ return { text: `\u26A0\uFE0F Could not update auto-update setting: ${result.error}` };
1392
1403
  }
1393
- },
1394
- required: ["query"]
1395
- },
1396
- execute: async (_toolCallId, rawParams) => {
1397
- const params = rawParams;
1398
- const dl = await getApi();
1399
- if (!dl) {
1400
1404
  return {
1401
- content: [{ type: "text", text: "Not logged in. Run /hivemind_login first." }]
1405
+ text: result.newValue ? "\u2705 Auto-update is ON. Hivemind will install new versions automatically when the gateway starts." : "\u23F8\uFE0F Auto-update is OFF. Run /hivemind_update manually to install new versions."
1402
1406
  };
1403
1407
  }
1404
- const targetPath = normalizeVirtualPath(params.path);
1405
- const grepParams = {
1406
- pattern: params.query,
1407
- ignoreCase: params.ignoreCase !== false,
1408
- wordMatch: false,
1409
- filesOnly: false,
1410
- countOnly: false,
1411
- lineNumber: false,
1412
- invertMatch: false,
1413
- fixedString: params.regex !== true
1414
- };
1415
- const searchOpts = buildGrepSearchOptions(grepParams, targetPath);
1416
- searchOpts.limit = Math.min(Math.max(params.limit ?? 20, 1), 100);
1417
- const t0 = Date.now();
1418
- try {
1419
- const rawRows = await searchDeeplakeTables(dl, memoryTable, sessionsTable, searchOpts);
1420
- const matchedRows = searchOpts.contentScanOnly ? (() => {
1421
- const re = compileGrepRegex(grepParams);
1422
- return rawRows.filter((r) => re.test(normalizeContent(r.path, r.content)));
1423
- })() : rawRows;
1424
- pluginApi.logger.info?.(`hivemind_search "${params.query.slice(0, 60)}" \u2192 ${matchedRows.length}/${rawRows.length} hits in ${Date.now() - t0}ms`);
1425
- if (matchedRows.length === 0) {
1426
- return { content: [{ type: "text", text: `No memory matches for "${params.query}" under ${targetPath}.` }] };
1408
+ });
1409
+ pluginApi.registerTool({
1410
+ name: "hivemind_search",
1411
+ label: "Hivemind Search",
1412
+ description: "Search Hivemind shared memory (summaries + past session turns) for keywords, phrases, or regex. Returns matching path + snippet pairs from BOTH the memory and sessions tables. Use this FIRST when the user asks about past work, decisions, people, or anything that might live in memory.",
1413
+ parameters: {
1414
+ type: "object",
1415
+ additionalProperties: false,
1416
+ properties: {
1417
+ query: {
1418
+ type: "string",
1419
+ minLength: 1,
1420
+ description: "Search text. Treated as a literal substring by default; set `regex: true` to use regex metacharacters."
1421
+ },
1422
+ path: {
1423
+ type: "string",
1424
+ description: "Optional virtual path prefix to scope the search, e.g. '/summaries/' or '/sessions/alice/'. Defaults to '/' (all of memory)."
1425
+ },
1426
+ regex: {
1427
+ type: "boolean",
1428
+ description: "If true, `query` is interpreted as a regex. Default false (literal substring)."
1429
+ },
1430
+ ignoreCase: {
1431
+ type: "boolean",
1432
+ description: "Case-insensitive match. Default true."
1433
+ },
1434
+ limit: {
1435
+ type: "integer",
1436
+ minimum: 1,
1437
+ maximum: 100,
1438
+ description: "Max rows returned per table. Default 20."
1439
+ }
1440
+ },
1441
+ required: ["query"]
1442
+ },
1443
+ execute: async (_toolCallId, rawParams) => {
1444
+ const params = rawParams;
1445
+ const dl = await getApi();
1446
+ if (!dl) {
1447
+ return {
1448
+ content: [{ type: "text", text: "Not logged in. Run /hivemind_login first." }]
1449
+ };
1427
1450
  }
1428
- const text = matchedRows.map((r, i) => {
1429
- const body = normalizeContent(r.path, r.content);
1430
- return `${i + 1}. ${r.path}
1451
+ const targetPath = normalizeVirtualPath(params.path);
1452
+ const grepParams = {
1453
+ pattern: params.query,
1454
+ ignoreCase: params.ignoreCase !== false,
1455
+ wordMatch: false,
1456
+ filesOnly: false,
1457
+ countOnly: false,
1458
+ lineNumber: false,
1459
+ invertMatch: false,
1460
+ fixedString: params.regex !== true
1461
+ };
1462
+ const searchOpts = buildGrepSearchOptions(grepParams, targetPath);
1463
+ searchOpts.limit = Math.min(Math.max(params.limit ?? 20, 1), 100);
1464
+ const t0 = Date.now();
1465
+ try {
1466
+ const rawRows = await searchDeeplakeTables(dl, memoryTable, sessionsTable, searchOpts);
1467
+ const matchedRows = searchOpts.contentScanOnly ? (() => {
1468
+ const re = compileGrepRegex(grepParams);
1469
+ return rawRows.filter((r) => re.test(normalizeContent(r.path, r.content)));
1470
+ })() : rawRows;
1471
+ pluginApi.logger.info?.(`hivemind_search "${params.query.slice(0, 60)}" \u2192 ${matchedRows.length}/${rawRows.length} hits in ${Date.now() - t0}ms`);
1472
+ if (matchedRows.length === 0) {
1473
+ return { content: [{ type: "text", text: `No memory matches for "${params.query}" under ${targetPath}.` }] };
1474
+ }
1475
+ const text = matchedRows.map((r, i) => {
1476
+ const body = normalizeContent(r.path, r.content);
1477
+ return `${i + 1}. ${r.path}
1431
1478
  ${body.slice(0, 500)}`;
1432
- }).join("\n\n");
1433
- return { content: [{ type: "text", text }], details: { hits: matchedRows.length, path: targetPath } };
1434
- } catch (err) {
1435
- const msg = err instanceof Error ? err.message : String(err);
1436
- pluginApi.logger.error(`hivemind_search failed: ${msg}`);
1437
- return { content: [{ type: "text", text: `Search failed: ${msg}` }] };
1438
- }
1439
- }
1440
- });
1441
- pluginApi.registerTool({
1442
- name: "hivemind_read",
1443
- label: "Hivemind Read",
1444
- description: "Read the full content of a specific Hivemind memory path (e.g. '/summaries/alice/abc.md' or '/sessions/alice/alice_org_ws_xyz.jsonl' or '/index.md'). Use this after hivemind_search to drill into a hit, or after hivemind_index to fetch a specific session.",
1445
- parameters: {
1446
- type: "object",
1447
- additionalProperties: false,
1448
- properties: {
1449
- path: {
1450
- type: "string",
1451
- minLength: 1,
1452
- description: "Virtual path under /summaries/, /sessions/, or '/index.md' for the memory index."
1479
+ }).join("\n\n");
1480
+ return { content: [{ type: "text", text }], details: { hits: matchedRows.length, path: targetPath } };
1481
+ } catch (err) {
1482
+ const msg = err instanceof Error ? err.message : String(err);
1483
+ pluginApi.logger.error(`hivemind_search failed: ${msg}`);
1484
+ return { content: [{ type: "text", text: `Search failed: ${msg}` }] };
1453
1485
  }
1454
- },
1455
- required: ["path"]
1456
- },
1457
- execute: async (_toolCallId, rawParams) => {
1458
- const params = rawParams;
1459
- const dl = await getApi();
1460
- if (!dl) {
1461
- return { content: [{ type: "text", text: "Not logged in. Run /hivemind_login first." }] };
1462
1486
  }
1463
- const virtualPath = normalizeVirtualPath(params.path);
1464
- try {
1465
- const content = await readVirtualPathContent(dl, memoryTable, sessionsTable, virtualPath);
1466
- if (content === null) {
1467
- return { content: [{ type: "text", text: `No content at ${virtualPath}.` }] };
1487
+ });
1488
+ pluginApi.registerTool({
1489
+ name: "hivemind_read",
1490
+ label: "Hivemind Read",
1491
+ description: "Read the full content of a specific Hivemind memory path (e.g. '/summaries/alice/abc.md' or '/sessions/alice/alice_org_ws_xyz.jsonl' or '/index.md'). Use this after hivemind_search to drill into a hit, or after hivemind_index to fetch a specific session.",
1492
+ parameters: {
1493
+ type: "object",
1494
+ additionalProperties: false,
1495
+ properties: {
1496
+ path: {
1497
+ type: "string",
1498
+ minLength: 1,
1499
+ description: "Virtual path under /summaries/, /sessions/, or '/index.md' for the memory index."
1500
+ }
1501
+ },
1502
+ required: ["path"]
1503
+ },
1504
+ execute: async (_toolCallId, rawParams) => {
1505
+ const params = rawParams;
1506
+ const dl = await getApi();
1507
+ if (!dl) {
1508
+ return { content: [{ type: "text", text: "Not logged in. Run /hivemind_login first." }] };
1509
+ }
1510
+ const virtualPath = normalizeVirtualPath(params.path);
1511
+ try {
1512
+ const content = await readVirtualPathContent(dl, memoryTable, sessionsTable, virtualPath);
1513
+ if (content === null) {
1514
+ return { content: [{ type: "text", text: `No content at ${virtualPath}.` }] };
1515
+ }
1516
+ return { content: [{ type: "text", text: content }], details: { path: virtualPath } };
1517
+ } catch (err) {
1518
+ const msg = err instanceof Error ? err.message : String(err);
1519
+ pluginApi.logger.error(`hivemind_read failed: ${msg}`);
1520
+ return { content: [{ type: "text", text: `Read failed: ${msg}` }] };
1468
1521
  }
1469
- return { content: [{ type: "text", text: content }], details: { path: virtualPath } };
1470
- } catch (err) {
1471
- const msg = err instanceof Error ? err.message : String(err);
1472
- pluginApi.logger.error(`hivemind_read failed: ${msg}`);
1473
- return { content: [{ type: "text", text: `Read failed: ${msg}` }] };
1474
- }
1475
- }
1476
- });
1477
- pluginApi.registerTool({
1478
- name: "hivemind_index",
1479
- label: "Hivemind Index",
1480
- description: "List every summary and session available in Hivemind (with paths, dates, descriptions). Use this when the user asks 'what's in memory?' or you don't know where to start looking.",
1481
- parameters: {
1482
- type: "object",
1483
- additionalProperties: false,
1484
- properties: {}
1485
- },
1486
- execute: async () => {
1487
- const dl = await getApi();
1488
- if (!dl) {
1489
- return { content: [{ type: "text", text: "Not logged in. Run /hivemind_login first." }] };
1490
- }
1491
- try {
1492
- const text = await readVirtualPathContent(dl, memoryTable, sessionsTable, "/index.md");
1493
- return { content: [{ type: "text", text: text ?? "(memory is empty)" }] };
1494
- } catch (err) {
1495
- const msg = err instanceof Error ? err.message : String(err);
1496
- pluginApi.logger.error(`hivemind_index failed: ${msg}`);
1497
- return { content: [{ type: "text", text: `Index build failed: ${msg}` }] };
1498
1522
  }
1499
- }
1500
- });
1501
- pluginApi.registerMemoryCorpusSupplement({
1502
- search: async ({ query, maxResults }) => {
1503
- const dl = await getApi();
1504
- if (!dl) return [];
1505
- const grepParams = {
1506
- pattern: query,
1507
- ignoreCase: true,
1508
- wordMatch: false,
1509
- filesOnly: false,
1510
- countOnly: false,
1511
- lineNumber: false,
1512
- invertMatch: false,
1513
- fixedString: true
1514
- };
1515
- const searchOpts = buildGrepSearchOptions(grepParams, "/");
1516
- searchOpts.limit = Math.min(Math.max(maxResults ?? 10, 1), 50);
1517
- try {
1518
- const rows = await searchDeeplakeTables(dl, memoryTable, sessionsTable, searchOpts);
1519
- return rows.map((r, i) => ({
1520
- path: r.path,
1521
- snippet: normalizeContent(r.path, r.content).slice(0, 400),
1522
- corpus: "hivemind",
1523
- kind: r.path.startsWith("/summaries/") ? "summary" : "session",
1524
- score: r.path.startsWith("/summaries/") ? 0.8 - i * 5e-3 : 0.6 - i * 5e-3
1525
- }));
1526
- } catch {
1527
- return [];
1523
+ });
1524
+ pluginApi.registerTool({
1525
+ name: "hivemind_index",
1526
+ label: "Hivemind Index",
1527
+ description: "List every summary and session available in Hivemind (with paths, dates, descriptions). Use this when the user asks 'what's in memory?' or you don't know where to start looking.",
1528
+ parameters: {
1529
+ type: "object",
1530
+ additionalProperties: false,
1531
+ properties: {}
1532
+ },
1533
+ execute: async () => {
1534
+ const dl = await getApi();
1535
+ if (!dl) {
1536
+ return { content: [{ type: "text", text: "Not logged in. Run /hivemind_login first." }] };
1537
+ }
1538
+ try {
1539
+ const text = await readVirtualPathContent(dl, memoryTable, sessionsTable, "/index.md");
1540
+ return { content: [{ type: "text", text: text ?? "(memory is empty)" }] };
1541
+ } catch (err) {
1542
+ const msg = err instanceof Error ? err.message : String(err);
1543
+ pluginApi.logger.error(`hivemind_index failed: ${msg}`);
1544
+ return { content: [{ type: "text", text: `Index build failed: ${msg}` }] };
1545
+ }
1528
1546
  }
1529
- },
1530
- get: async ({ lookup }) => {
1531
- const dl = await getApi();
1532
- if (!dl) return null;
1533
- try {
1534
- const content = await readVirtualPathContent(dl, memoryTable, sessionsTable, normalizeVirtualPath(lookup));
1535
- return content === null ? null : { path: lookup, content };
1536
- } catch {
1537
- return null;
1547
+ });
1548
+ pluginApi.registerMemoryCorpusSupplement({
1549
+ search: async ({ query, maxResults }) => {
1550
+ const dl = await getApi();
1551
+ if (!dl) return [];
1552
+ const grepParams = {
1553
+ pattern: query,
1554
+ ignoreCase: true,
1555
+ wordMatch: false,
1556
+ filesOnly: false,
1557
+ countOnly: false,
1558
+ lineNumber: false,
1559
+ invertMatch: false,
1560
+ fixedString: true
1561
+ };
1562
+ const searchOpts = buildGrepSearchOptions(grepParams, "/");
1563
+ searchOpts.limit = Math.min(Math.max(maxResults ?? 10, 1), 50);
1564
+ try {
1565
+ const rows = await searchDeeplakeTables(dl, memoryTable, sessionsTable, searchOpts);
1566
+ return rows.map((r, i) => ({
1567
+ path: r.path,
1568
+ snippet: normalizeContent(r.path, r.content).slice(0, 400),
1569
+ corpus: "hivemind",
1570
+ kind: r.path.startsWith("/summaries/") ? "summary" : "session",
1571
+ score: r.path.startsWith("/summaries/") ? 0.8 - i * 5e-3 : 0.6 - i * 5e-3
1572
+ }));
1573
+ } catch {
1574
+ return [];
1575
+ }
1576
+ },
1577
+ get: async ({ lookup }) => {
1578
+ const dl = await getApi();
1579
+ if (!dl) return null;
1580
+ try {
1581
+ const content = await readVirtualPathContent(dl, memoryTable, sessionsTable, normalizeVirtualPath(lookup));
1582
+ return content === null ? null : { path: lookup, content };
1583
+ } catch {
1584
+ return null;
1585
+ }
1538
1586
  }
1587
+ });
1588
+ const config = pluginApi.pluginConfig ?? {};
1589
+ const logger = pluginApi.logger;
1590
+ const hook = (event, handler) => {
1591
+ pluginApi.on(event, handler);
1592
+ };
1593
+ if (config.autoUpdate !== false) {
1594
+ (async () => {
1595
+ try {
1596
+ const current = getInstalledVersion();
1597
+ if (!current) return;
1598
+ const res = await fetch(VERSION_URL, { signal: AbortSignal.timeout(3e3) });
1599
+ if (!res.ok) return;
1600
+ const latest = extractLatestVersion(await res.json());
1601
+ if (!latest || !isNewer(latest, current)) return;
1602
+ pendingUpdate = { current, latest };
1603
+ logger.info?.(`Hivemind update available: ${current} \u2192 ${latest}. Agent will be prompted to install when user asks.`);
1604
+ } catch (err) {
1605
+ logger.error(`Auto-update check failed: ${err instanceof Error ? err.message : String(err)}`);
1606
+ }
1607
+ })();
1539
1608
  }
1540
- });
1541
- const config = pluginApi.pluginConfig ?? {};
1542
- const logger = pluginApi.logger;
1543
- const hook = (event, handler) => {
1544
- pluginApi.on(event, handler);
1545
- };
1546
- if (config.autoUpdate !== false) {
1547
- (async () => {
1548
- try {
1549
- const current = getInstalledVersion();
1550
- if (!current) return;
1551
- const res = await fetch(VERSION_URL, { signal: AbortSignal.timeout(3e3) });
1552
- if (!res.ok) return;
1553
- const latest = extractLatestVersion(await res.json());
1554
- if (!latest || !isNewer(latest, current)) return;
1555
- pendingUpdate = { current, latest };
1556
- logger.info?.(`Hivemind update available: ${current} \u2192 ${latest}. Agent will be prompted to install when user asks.`);
1557
- } catch (err) {
1558
- logger.error(`Auto-update check failed: ${err instanceof Error ? err.message : String(err)}`);
1559
- }
1560
- })();
1561
- }
1562
- if ('---\nname: hivemind\ndescription: Global team and org memory powered by Activeloop. ALWAYS check BOTH built-in memory AND Hivemind memory when recalling information.\nallowed-tools: hivemind_search, hivemind_read, hivemind_index\n---\n\n# Hivemind Memory\n\nYou have TWO memory sources. ALWAYS check BOTH when the user asks you to recall, remember, or look up ANY information:\n\n1. **Your built-in memory** \u2014 personal per-project notes from the host agent\n2. **Hivemind global memory** \u2014 global memory shared across all sessions, users, and agents in the org, accessed via the tools below\n\n## Memory Structure\n\n```\n/index.md \u2190 START HERE \u2014 table of all sessions\n/summaries/\n <username>/\n <session-id>.md \u2190 AI-generated wiki summary per session\n/sessions/\n <username>/\n <user_org_ws_slug>.jsonl \u2190 raw session data\n```\n\n## How to Search\n\n1. **First**: call `hivemind_index()` \u2014 table of all sessions with dates, projects, descriptions\n2. **If you need details**: call `hivemind_read("/summaries/<username>/<session>.md")`\n3. **If you need raw data**: call `hivemind_read("/sessions/<username>/<file>.jsonl")`\n4. **Keyword search**: call `hivemind_search("keyword")` \u2014 substring search across both summaries and sessions, returns `path:line` hits\n\nDo NOT jump straight to reading raw JSONL files. Always start with `hivemind_index` and summaries.\n\n## Organization Management\n\n- `/hivemind_login` \u2014 sign in via device flow\n- `/hivemind_capture` \u2014 toggle capture on/off (off = no data sent)\n- `/hivemind_whoami` \u2014 show current org and workspace\n- `/hivemind_orgs` \u2014 list organizations\n- `/hivemind_switch_org <name-or-id>` \u2014 switch organization\n- `/hivemind_workspaces` \u2014 list workspaces\n- `/hivemind_switch_workspace <id>` \u2014 switch workspace\n- `/hivemind_version` \u2014 show installed version and check ClawHub for updates\n- `/hivemind_update` \u2014 shows how to install (ask the agent, or run `openclaw plugins update hivemind` in your terminal)\n- `/hivemind_autoupdate [on|off]` \u2014 toggle the agent-facing update nudge (on by default: when a newer version is available, the agent is prompted to install it via `exec` if you ask to update)\n\n## Limits\n\nDo NOT delegate to subagents when reading Hivemind memory. If a tool call returns empty after 2 attempts, skip it and move on. Report what you found rather than exhaustively retrying.\n\n## Getting Started\n\nAfter installing the plugin:\n1. Run `/hivemind_login` to authenticate\n2. Run `/hivemind_setup` to enable the memory tools in your openclaw allowlist (one-time, per install)\n3. Start using memory \u2014 ask questions, the agent automatically captures and searches\n\n## Sharing memory\n\nMultiple agents share memory when users are in the same Activeloop organization.\n'.length > 0) {
1563
- const allowlistNudge = detectAllowlistMissing() ? "\n\n<hivemind-setup-needed>\nThe user hasn't run /hivemind_setup yet, so hivemind_search, hivemind_read, and hivemind_index are NOT available to you. If they ask about memory and you can't help, tell them to run /hivemind_setup to enable Hivemind memory tools.\n</hivemind-setup-needed>\n" : "";
1564
- hook("before_prompt_build", async () => {
1565
- const updateNudge = pendingUpdate ? `
1609
+ if ('---\nname: hivemind\ndescription: Global team and org memory powered by Activeloop. ALWAYS check BOTH built-in memory AND Hivemind memory when recalling information.\nallowed-tools: hivemind_search, hivemind_read, hivemind_index\n---\n\n# Hivemind Memory\n\nYou have TWO memory sources. ALWAYS check BOTH when the user asks you to recall, remember, or look up ANY information:\n\n1. **Your built-in memory** \u2014 personal per-project notes from the host agent\n2. **Hivemind global memory** \u2014 global memory shared across all sessions, users, and agents in the org, accessed via the tools below\n\n## Memory Structure\n\n```\n/index.md \u2190 START HERE \u2014 table of all sessions\n/summaries/\n <username>/\n <session-id>.md \u2190 AI-generated wiki summary per session\n/sessions/\n <username>/\n <user_org_ws_slug>.jsonl \u2190 raw session data\n```\n\n## How to Search\n\n1. **First**: call `hivemind_index()` \u2014 table of all sessions with dates, projects, descriptions\n2. **If you need details**: call `hivemind_read("/summaries/<username>/<session>.md")`\n3. **If you need raw data**: call `hivemind_read("/sessions/<username>/<file>.jsonl")`\n4. **Keyword search**: call `hivemind_search("keyword")` \u2014 substring search across both summaries and sessions, returns `path:line` hits\n\nDo NOT jump straight to reading raw JSONL files. Always start with `hivemind_index` and summaries.\n\n## Organization Management\n\n- `/hivemind_login` \u2014 sign in via device flow\n- `/hivemind_capture` \u2014 toggle capture on/off (off = no data sent)\n- `/hivemind_whoami` \u2014 show current org and workspace\n- `/hivemind_orgs` \u2014 list organizations\n- `/hivemind_switch_org <name-or-id>` \u2014 switch organization\n- `/hivemind_workspaces` \u2014 list workspaces\n- `/hivemind_switch_workspace <id>` \u2014 switch workspace\n- `/hivemind_version` \u2014 show installed version and check ClawHub for updates\n- `/hivemind_update` \u2014 shows how to install (ask the agent, or run `openclaw plugins update hivemind` in your terminal)\n- `/hivemind_autoupdate [on|off]` \u2014 toggle the agent-facing update nudge (on by default: when a newer version is available, the agent is prompted to install it via `exec` if you ask to update)\n\n## Limits\n\nDo NOT delegate to subagents when reading Hivemind memory. If a tool call returns empty after 2 attempts, skip it and move on. Report what you found rather than exhaustively retrying.\n\n## Getting Started\n\nAfter installing the plugin:\n1. Run `/hivemind_login` to authenticate\n2. Run `/hivemind_setup` to enable the memory tools in your openclaw allowlist (one-time, per install)\n3. Start using memory \u2014 ask questions, the agent automatically captures and searches\n\n## Sharing memory\n\nMultiple agents share memory when users are in the same Activeloop organization.\n'.length > 0) {
1610
+ const setupConfigPromise = loadSetupConfig();
1611
+ hook("before_prompt_build", async () => {
1612
+ const { detectAllowlistMissing } = await setupConfigPromise;
1613
+ const allowlistNudge = detectAllowlistMissing() ? "\n\n<hivemind-setup-needed>\nThe user hasn't run /hivemind_setup yet, so hivemind_search, hivemind_read, and hivemind_index are NOT available to you. If they ask about memory and you can't help, tell them to run /hivemind_setup to enable Hivemind memory tools.\n</hivemind-setup-needed>\n" : "";
1614
+ const updateNudge = pendingUpdate ? `
1566
1615
 
1567
1616
  <hivemind-update-available>
1568
1617
  A newer Hivemind version is available: ${pendingUpdate.current} \u2192 ${pendingUpdate.latest}. Install command: \`openclaw plugins update hivemind\`. The gateway reloads the plugin after install.
1569
1618
  </hivemind-update-available>
1570
1619
  ` : "";
1571
- return {
1572
- prependSystemContext: allowlistNudge + updateNudge + '\n\n<hivemind-skill>\n---\nname: hivemind\ndescription: Global team and org memory powered by Activeloop. ALWAYS check BOTH built-in memory AND Hivemind memory when recalling information.\nallowed-tools: hivemind_search, hivemind_read, hivemind_index\n---\n\n# Hivemind Memory\n\nYou have TWO memory sources. ALWAYS check BOTH when the user asks you to recall, remember, or look up ANY information:\n\n1. **Your built-in memory** \u2014 personal per-project notes from the host agent\n2. **Hivemind global memory** \u2014 global memory shared across all sessions, users, and agents in the org, accessed via the tools below\n\n## Memory Structure\n\n```\n/index.md \u2190 START HERE \u2014 table of all sessions\n/summaries/\n <username>/\n <session-id>.md \u2190 AI-generated wiki summary per session\n/sessions/\n <username>/\n <user_org_ws_slug>.jsonl \u2190 raw session data\n```\n\n## How to Search\n\n1. **First**: call `hivemind_index()` \u2014 table of all sessions with dates, projects, descriptions\n2. **If you need details**: call `hivemind_read("/summaries/<username>/<session>.md")`\n3. **If you need raw data**: call `hivemind_read("/sessions/<username>/<file>.jsonl")`\n4. **Keyword search**: call `hivemind_search("keyword")` \u2014 substring search across both summaries and sessions, returns `path:line` hits\n\nDo NOT jump straight to reading raw JSONL files. Always start with `hivemind_index` and summaries.\n\n## Organization Management\n\n- `/hivemind_login` \u2014 sign in via device flow\n- `/hivemind_capture` \u2014 toggle capture on/off (off = no data sent)\n- `/hivemind_whoami` \u2014 show current org and workspace\n- `/hivemind_orgs` \u2014 list organizations\n- `/hivemind_switch_org <name-or-id>` \u2014 switch organization\n- `/hivemind_workspaces` \u2014 list workspaces\n- `/hivemind_switch_workspace <id>` \u2014 switch workspace\n- `/hivemind_version` \u2014 show installed version and check ClawHub for updates\n- `/hivemind_update` \u2014 shows how to install (ask the agent, or run `openclaw plugins update hivemind` in your terminal)\n- `/hivemind_autoupdate [on|off]` \u2014 toggle the agent-facing update nudge (on by default: when a newer version is available, the agent is prompted to install it via `exec` if you ask to update)\n\n## Limits\n\nDo NOT delegate to subagents when reading Hivemind memory. If a tool call returns empty after 2 attempts, skip it and move on. Report what you found rather than exhaustively retrying.\n\n## Getting Started\n\nAfter installing the plugin:\n1. Run `/hivemind_login` to authenticate\n2. Run `/hivemind_setup` to enable the memory tools in your openclaw allowlist (one-time, per install)\n3. Start using memory \u2014 ask questions, the agent automatically captures and searches\n\n## Sharing memory\n\nMultiple agents share memory when users are in the same Activeloop organization.\n\n</hivemind-skill>\n'
1573
- };
1574
- });
1575
- }
1576
- if (config.autoRecall !== false) {
1577
- hook("before_agent_start", async (event) => {
1578
- if (!event.prompt || event.prompt.length < 5) return;
1579
- try {
1580
- const dl = await getApi();
1581
- if (!dl && authUrl) {
1582
- return {
1583
- prependContext: `
1620
+ return {
1621
+ prependSystemContext: allowlistNudge + updateNudge + '\n\n<hivemind-skill>\n---\nname: hivemind\ndescription: Global team and org memory powered by Activeloop. ALWAYS check BOTH built-in memory AND Hivemind memory when recalling information.\nallowed-tools: hivemind_search, hivemind_read, hivemind_index\n---\n\n# Hivemind Memory\n\nYou have TWO memory sources. ALWAYS check BOTH when the user asks you to recall, remember, or look up ANY information:\n\n1. **Your built-in memory** \u2014 personal per-project notes from the host agent\n2. **Hivemind global memory** \u2014 global memory shared across all sessions, users, and agents in the org, accessed via the tools below\n\n## Memory Structure\n\n```\n/index.md \u2190 START HERE \u2014 table of all sessions\n/summaries/\n <username>/\n <session-id>.md \u2190 AI-generated wiki summary per session\n/sessions/\n <username>/\n <user_org_ws_slug>.jsonl \u2190 raw session data\n```\n\n## How to Search\n\n1. **First**: call `hivemind_index()` \u2014 table of all sessions with dates, projects, descriptions\n2. **If you need details**: call `hivemind_read("/summaries/<username>/<session>.md")`\n3. **If you need raw data**: call `hivemind_read("/sessions/<username>/<file>.jsonl")`\n4. **Keyword search**: call `hivemind_search("keyword")` \u2014 substring search across both summaries and sessions, returns `path:line` hits\n\nDo NOT jump straight to reading raw JSONL files. Always start with `hivemind_index` and summaries.\n\n## Organization Management\n\n- `/hivemind_login` \u2014 sign in via device flow\n- `/hivemind_capture` \u2014 toggle capture on/off (off = no data sent)\n- `/hivemind_whoami` \u2014 show current org and workspace\n- `/hivemind_orgs` \u2014 list organizations\n- `/hivemind_switch_org <name-or-id>` \u2014 switch organization\n- `/hivemind_workspaces` \u2014 list workspaces\n- `/hivemind_switch_workspace <id>` \u2014 switch workspace\n- `/hivemind_version` \u2014 show installed version and check ClawHub for updates\n- `/hivemind_update` \u2014 shows how to install (ask the agent, or run `openclaw plugins update hivemind` in your terminal)\n- `/hivemind_autoupdate [on|off]` \u2014 toggle the agent-facing update nudge (on by default: when a newer version is available, the agent is prompted to install it via `exec` if you ask to update)\n\n## Limits\n\nDo NOT delegate to subagents when reading Hivemind memory. If a tool call returns empty after 2 attempts, skip it and move on. Report what you found rather than exhaustively retrying.\n\n## Getting Started\n\nAfter installing the plugin:\n1. Run `/hivemind_login` to authenticate\n2. Run `/hivemind_setup` to enable the memory tools in your openclaw allowlist (one-time, per install)\n3. Start using memory \u2014 ask questions, the agent automatically captures and searches\n\n## Sharing memory\n\nMultiple agents share memory when users are in the same Activeloop organization.\n\n</hivemind-skill>\n'
1622
+ };
1623
+ });
1624
+ }
1625
+ if (config.autoRecall !== false) {
1626
+ hook("before_agent_start", async (event) => {
1627
+ if (!event.prompt || event.prompt.length < 5) return;
1628
+ try {
1629
+ const dl = await getApi();
1630
+ if (!dl && authUrl) {
1631
+ return {
1632
+ prependContext: `
1584
1633
 
1585
1634
  IMPORTANT: The user needs to authenticate with Hivemind. Include this link in your reply: ${authUrl}
1586
1635
  Tell them to click it and sign in, then send another message.
1587
1636
  `
1588
- };
1589
- }
1590
- if (!dl) return;
1591
- if (justAuthenticated) {
1592
- justAuthenticated = false;
1593
- const creds2 = loadCredentials();
1594
- const orgName = creds2?.orgName ?? creds2?.orgId ?? "unknown";
1595
- return { prependContext: `
1637
+ };
1638
+ }
1639
+ if (!dl) return;
1640
+ if (justAuthenticated) {
1641
+ justAuthenticated = false;
1642
+ const creds2 = await loadCredentials2();
1643
+ const orgName = creds2?.orgName ?? creds2?.orgId ?? "unknown";
1644
+ return { prependContext: `
1596
1645
 
1597
1646
  \u{1F41D} Welcome to Hivemind!
1598
1647
 
@@ -1607,106 +1656,107 @@ Get started:
1607
1656
 
1608
1657
  One brain for every agent on your team.
1609
1658
  ` };
1610
- }
1611
- const keywords = extractKeywords(event.prompt);
1612
- if (!keywords.length) return;
1613
- const grepParams = {
1614
- pattern: keywords.join(" "),
1615
- ignoreCase: true,
1616
- wordMatch: false,
1617
- filesOnly: false,
1618
- countOnly: false,
1619
- lineNumber: false,
1620
- invertMatch: false,
1621
- fixedString: true
1622
- };
1623
- const searchOpts = buildGrepSearchOptions(grepParams, "/");
1624
- searchOpts.limit = 10;
1625
- const rows = await searchDeeplakeTables(dl, memoryTable, sessionsTable, searchOpts);
1626
- if (!rows.length) return;
1627
- const recalled = rows.map((r) => {
1628
- const body = normalizeContent(r.path, r.content);
1629
- return `[${r.path}] ${body.slice(0, 400)}`;
1630
- }).join("\n\n");
1631
- logger.info?.(`Auto-recalled ${rows.length} memories`);
1632
- const instruction = "These are raw Hivemind search hits from prior sessions. Each hit is prefixed with its path (e.g. `/summaries/<username>/...`). Different usernames are different people \u2014 do NOT merge, alias, or conflate them. If you need more detail, call `hivemind_search` with a more specific query or `hivemind_read` on a specific path. If these hits don't answer the question, say so rather than guessing.";
1633
- return {
1634
- prependContext: "\n\n<recalled-memories>\n" + instruction + "\n\n" + recalled + "\n</recalled-memories>\n"
1635
- };
1636
- } catch (err) {
1637
- logger.error(`Auto-recall failed: ${err instanceof Error ? err.message : String(err)}`);
1638
- }
1639
- });
1640
- }
1641
- if (config.autoCapture !== false) {
1642
- hook("agent_end", async (event) => {
1643
- const ev = event;
1644
- if (!captureEnabled || !ev.success || !ev.messages?.length) return;
1645
- try {
1646
- const dl = await getApi();
1647
- if (!dl) return;
1648
- const cfg = loadConfig();
1649
- if (!cfg) return;
1650
- const sid = ev.session_id || fallbackSessionId;
1651
- const lastCount = capturedCounts.get(sid) ?? 0;
1652
- const newMessages = ev.messages.slice(lastCount);
1653
- capturedCounts.set(sid, ev.messages.length);
1654
- if (!newMessages.length) return;
1655
- const sessionPath = buildSessionPath(cfg, sid);
1656
- const filename = sessionPath.split("/").pop() ?? "";
1657
- const projectName = ev.channel || "openclaw";
1658
- for (const msg of newMessages) {
1659
- if (msg.role !== "user" && msg.role !== "assistant") continue;
1660
- let text = "";
1661
- if (typeof msg.content === "string") {
1662
- text = msg.content;
1663
- } else if (Array.isArray(msg.content)) {
1664
- text = msg.content.filter((b) => b.type === "text" && b.text).map((b) => b.text).join("\n");
1665
1659
  }
1666
- if (!text.trim()) continue;
1667
- const ts = (/* @__PURE__ */ new Date()).toISOString();
1668
- const entry = {
1669
- id: crypto.randomUUID(),
1670
- type: msg.role === "user" ? "user_message" : "assistant_message",
1671
- session_id: sid,
1672
- content: text,
1673
- timestamp: ts
1660
+ const keywords = extractKeywords(event.prompt);
1661
+ if (!keywords.length) return;
1662
+ const grepParams = {
1663
+ pattern: keywords.join(" "),
1664
+ ignoreCase: true,
1665
+ wordMatch: false,
1666
+ filesOnly: false,
1667
+ countOnly: false,
1668
+ lineNumber: false,
1669
+ invertMatch: false,
1670
+ fixedString: true
1674
1671
  };
1675
- const line = JSON.stringify(entry);
1676
- const jsonForSql = line.replace(/'/g, "''");
1677
- const insertSql = `INSERT INTO "${sessionsTable}" (id, path, filename, message, author, size_bytes, project, description, agent, creation_date, last_update_date) VALUES ('${crypto.randomUUID()}', '${sqlStr(sessionPath)}', '${sqlStr(filename)}', '${jsonForSql}'::jsonb, '${sqlStr(cfg.userName)}', ${Buffer.byteLength(line, "utf-8")}, '${sqlStr(projectName)}', '${sqlStr(msg.role)}', 'openclaw', '${ts}', '${ts}')`;
1678
- try {
1679
- await dl.query(insertSql);
1680
- } catch (e) {
1681
- if (e.message?.includes("permission denied") || e.message?.includes("does not exist")) {
1682
- await dl.ensureSessionsTable(sessionsTable);
1672
+ const searchOpts = buildGrepSearchOptions(grepParams, "/");
1673
+ searchOpts.limit = 10;
1674
+ const rows = await searchDeeplakeTables(dl, memoryTable, sessionsTable, searchOpts);
1675
+ if (!rows.length) return;
1676
+ const recalled = rows.map((r) => {
1677
+ const body = normalizeContent(r.path, r.content);
1678
+ return `[${r.path}] ${body.slice(0, 400)}`;
1679
+ }).join("\n\n");
1680
+ logger.info?.(`Auto-recalled ${rows.length} memories`);
1681
+ const instruction = "These are raw Hivemind search hits from prior sessions. Each hit is prefixed with its path (e.g. `/summaries/<username>/...`). Different usernames are different people \u2014 do NOT merge, alias, or conflate them. If you need more detail, call `hivemind_search` with a more specific query or `hivemind_read` on a specific path. If these hits don't answer the question, say so rather than guessing.";
1682
+ return {
1683
+ prependContext: "\n\n<recalled-memories>\n" + instruction + "\n\n" + recalled + "\n</recalled-memories>\n"
1684
+ };
1685
+ } catch (err) {
1686
+ logger.error(`Auto-recall failed: ${err instanceof Error ? err.message : String(err)}`);
1687
+ }
1688
+ });
1689
+ }
1690
+ if (config.autoCapture !== false) {
1691
+ hook("agent_end", async (event) => {
1692
+ const ev = event;
1693
+ if (!captureEnabled || !ev.success || !ev.messages?.length) return;
1694
+ try {
1695
+ const dl = await getApi();
1696
+ if (!dl) return;
1697
+ const cfg = await loadConfig();
1698
+ if (!cfg) return;
1699
+ const sid = ev.session_id || fallbackSessionId;
1700
+ const lastCount = capturedCounts.get(sid) ?? 0;
1701
+ const newMessages = ev.messages.slice(lastCount);
1702
+ capturedCounts.set(sid, ev.messages.length);
1703
+ if (!newMessages.length) return;
1704
+ const sessionPath = buildSessionPath(cfg, sid);
1705
+ const filename = sessionPath.split("/").pop() ?? "";
1706
+ const projectName = ev.channel || "openclaw";
1707
+ for (const msg of newMessages) {
1708
+ if (msg.role !== "user" && msg.role !== "assistant") continue;
1709
+ let text = "";
1710
+ if (typeof msg.content === "string") {
1711
+ text = msg.content;
1712
+ } else if (Array.isArray(msg.content)) {
1713
+ text = msg.content.filter((b) => b.type === "text" && b.text).map((b) => b.text).join("\n");
1714
+ }
1715
+ if (!text.trim()) continue;
1716
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
1717
+ const entry = {
1718
+ id: crypto.randomUUID(),
1719
+ type: msg.role === "user" ? "user_message" : "assistant_message",
1720
+ session_id: sid,
1721
+ content: text,
1722
+ timestamp: ts
1723
+ };
1724
+ const line = JSON.stringify(entry);
1725
+ const jsonForSql = line.replace(/'/g, "''");
1726
+ const insertSql = `INSERT INTO "${sessionsTable}" (id, path, filename, message, author, size_bytes, project, description, agent, creation_date, last_update_date) VALUES ('${crypto.randomUUID()}', '${sqlStr(sessionPath)}', '${sqlStr(filename)}', '${jsonForSql}'::jsonb, '${sqlStr(cfg.userName)}', ${Buffer.byteLength(line, "utf-8")}, '${sqlStr(projectName)}', '${sqlStr(msg.role)}', 'openclaw', '${ts}', '${ts}')`;
1727
+ try {
1683
1728
  await dl.query(insertSql);
1684
- } else {
1685
- throw e;
1729
+ } catch (e) {
1730
+ if (e.message?.includes("permission denied") || e.message?.includes("does not exist")) {
1731
+ await dl.ensureSessionsTable(sessionsTable);
1732
+ await dl.query(insertSql);
1733
+ } else {
1734
+ throw e;
1735
+ }
1686
1736
  }
1687
1737
  }
1738
+ logger.info?.(`Auto-captured ${newMessages.length} messages`);
1739
+ } catch (err) {
1740
+ logger.error(`Auto-capture failed: ${err instanceof Error ? err.message : String(err)}`);
1688
1741
  }
1689
- logger.info?.(`Auto-captured ${newMessages.length} messages`);
1690
- } catch (err) {
1691
- logger.error(`Auto-capture failed: ${err instanceof Error ? err.message : String(err)}`);
1692
- }
1693
- });
1694
- }
1695
- const creds = loadCredentials();
1696
- if (!creds?.token) {
1697
- logger.info?.("Hivemind installed. Run /hivemind_login to authenticate and activate shared memory.");
1698
- if (!authPending) {
1699
- requestAuth().catch((err) => {
1700
- logger.error(`Pre-auth failed: ${err instanceof Error ? err.message : String(err)}`);
1701
1742
  });
1702
1743
  }
1744
+ const creds = await loadCredentials2();
1745
+ if (!creds?.token) {
1746
+ logger.info?.("Hivemind installed. Run /hivemind_login to authenticate and activate shared memory.");
1747
+ if (!authPending) {
1748
+ requestAuth().catch((err) => {
1749
+ logger.error(`Pre-auth failed: ${err instanceof Error ? err.message : String(err)}`);
1750
+ });
1751
+ }
1752
+ }
1753
+ checkForUpdate(logger).catch(() => {
1754
+ });
1755
+ logger.info?.("Hivemind plugin registered");
1756
+ } catch (err) {
1757
+ pluginApi.logger?.error?.(`Hivemind register failed: ${err instanceof Error ? err.message : String(err)}`);
1703
1758
  }
1704
- checkForUpdate(logger).catch(() => {
1705
- });
1706
- logger.info?.("Hivemind plugin registered");
1707
- } catch (err) {
1708
- pluginApi.logger?.error?.(`Hivemind register failed: ${err instanceof Error ? err.message : String(err)}`);
1709
- }
1759
+ })();
1710
1760
  }
1711
1761
  });
1712
1762
  export {