@deeplake/hivemind 0.6.48 → 0.7.9
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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +244 -20
- package/bundle/cli.js +1369 -112
- package/codex/bundle/capture.js +546 -96
- package/codex/bundle/commands/auth-login.js +290 -81
- package/codex/bundle/embeddings/embed-daemon.js +243 -0
- package/codex/bundle/pre-tool-use.js +666 -111
- package/codex/bundle/session-start-setup.js +231 -64
- package/codex/bundle/session-start.js +52 -13
- package/codex/bundle/shell/deeplake-shell.js +716 -119
- package/codex/bundle/skilify-worker.js +907 -0
- package/codex/bundle/stop.js +819 -79
- package/codex/bundle/wiki-worker.js +312 -11
- package/cursor/bundle/capture.js +1116 -64
- package/cursor/bundle/commands/auth-login.js +290 -81
- package/cursor/bundle/embeddings/embed-daemon.js +243 -0
- package/cursor/bundle/pre-tool-use.js +598 -77
- package/cursor/bundle/session-end.js +520 -2
- package/cursor/bundle/session-start.js +257 -65
- package/cursor/bundle/shell/deeplake-shell.js +716 -119
- package/cursor/bundle/skilify-worker.js +907 -0
- package/cursor/bundle/wiki-worker.js +571 -0
- package/hermes/bundle/capture.js +1119 -65
- package/hermes/bundle/commands/auth-login.js +290 -81
- package/hermes/bundle/embeddings/embed-daemon.js +243 -0
- package/hermes/bundle/pre-tool-use.js +597 -76
- package/hermes/bundle/session-end.js +522 -1
- package/hermes/bundle/session-start.js +260 -65
- package/hermes/bundle/shell/deeplake-shell.js +716 -119
- package/hermes/bundle/skilify-worker.js +907 -0
- package/hermes/bundle/wiki-worker.js +572 -0
- package/mcp/bundle/server.js +290 -75
- package/openclaw/dist/chunks/auth-creds-AEKS6D3P.js +14 -0
- package/openclaw/dist/chunks/chunk-SRCBBT4H.js +37 -0
- package/openclaw/dist/chunks/config-ZLH6JFJS.js +34 -0
- package/openclaw/dist/chunks/index-marker-store-PGT5CW6T.js +33 -0
- package/openclaw/dist/chunks/setup-config-C35UK4LP.js +114 -0
- package/openclaw/dist/index.js +929 -710
- package/openclaw/dist/skilify-worker.js +907 -0
- package/openclaw/openclaw.plugin.json +1 -1
- package/openclaw/package.json +1 -1
- package/openclaw/skills/SKILL.md +19 -0
- package/package.json +7 -1
- package/pi/extension-source/hivemind.ts +603 -22
package/openclaw/dist/index.js
CHANGED
|
@@ -1,164 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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/
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
220
|
-
import { homedir
|
|
76
|
+
import { join } from "node:path";
|
|
77
|
+
import { homedir } from "node:os";
|
|
221
78
|
var DEBUG = false;
|
|
222
|
-
var 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}
|
|
@@ -233,8 +90,23 @@ function sqlStr(value) {
|
|
|
233
90
|
function sqlLike(value) {
|
|
234
91
|
return sqlStr(value).replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
235
92
|
}
|
|
93
|
+
function sqlIdent(name) {
|
|
94
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
|
|
95
|
+
throw new Error(`Invalid SQL identifier: ${JSON.stringify(name)}`);
|
|
96
|
+
}
|
|
97
|
+
return name;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/embeddings/columns.ts
|
|
101
|
+
var SUMMARY_EMBEDDING_COL = "summary_embedding";
|
|
102
|
+
var MESSAGE_EMBEDDING_COL = "message_embedding";
|
|
236
103
|
|
|
237
104
|
// src/deeplake-api.ts
|
|
105
|
+
var indexMarkerStorePromise = null;
|
|
106
|
+
function getIndexMarkerStore() {
|
|
107
|
+
if (!indexMarkerStorePromise) indexMarkerStorePromise = import("./chunks/index-marker-store-PGT5CW6T.js");
|
|
108
|
+
return indexMarkerStorePromise;
|
|
109
|
+
}
|
|
238
110
|
var log2 = (msg) => log("sdk", msg);
|
|
239
111
|
function summarizeSql(sql, maxLen = 220) {
|
|
240
112
|
const compact = sql.replace(/\s+/g, " ").trim();
|
|
@@ -252,7 +124,6 @@ var MAX_RETRIES = 3;
|
|
|
252
124
|
var BASE_DELAY_MS = 500;
|
|
253
125
|
var MAX_CONCURRENCY = 5;
|
|
254
126
|
var QUERY_TIMEOUT_MS = Number(1e4);
|
|
255
|
-
var INDEX_MARKER_TTL_MS = Number(6 * 60 * 6e4);
|
|
256
127
|
function sleep(ms) {
|
|
257
128
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
258
129
|
}
|
|
@@ -272,9 +143,6 @@ function isTransientHtml403(text) {
|
|
|
272
143
|
const body = text.toLowerCase();
|
|
273
144
|
return body.includes("<html") || body.includes("403 forbidden") || body.includes("cloudflare") || body.includes("nginx");
|
|
274
145
|
}
|
|
275
|
-
function getIndexMarkerDir() {
|
|
276
|
-
return join5(tmpdir(), "hivemind-deeplake-indexes");
|
|
277
|
-
}
|
|
278
146
|
var Semaphore = class {
|
|
279
147
|
constructor(max) {
|
|
280
148
|
this.max = max;
|
|
@@ -343,7 +211,8 @@ var DeeplakeApi = class {
|
|
|
343
211
|
headers: {
|
|
344
212
|
Authorization: `Bearer ${this.token}`,
|
|
345
213
|
"Content-Type": "application/json",
|
|
346
|
-
"X-Activeloop-Org-Id": this.orgId
|
|
214
|
+
"X-Activeloop-Org-Id": this.orgId,
|
|
215
|
+
...deeplakeClientHeader()
|
|
347
216
|
},
|
|
348
217
|
signal,
|
|
349
218
|
body: JSON.stringify({ query: sql })
|
|
@@ -371,7 +240,8 @@ var DeeplakeApi = class {
|
|
|
371
240
|
}
|
|
372
241
|
const text = await resp.text().catch(() => "");
|
|
373
242
|
const retryable403 = isSessionInsertQuery(sql) && (resp.status === 401 || resp.status === 403 && (text.length === 0 || isTransientHtml403(text)));
|
|
374
|
-
|
|
243
|
+
const alreadyExists = resp.status === 500 && isDuplicateIndexError(text);
|
|
244
|
+
if (!alreadyExists && attempt < MAX_RETRIES && (RETRYABLE_CODES.has(resp.status) || retryable403)) {
|
|
375
245
|
const delay = BASE_DELAY_MS * Math.pow(2, attempt) + Math.random() * 200;
|
|
376
246
|
log2(`query retry ${attempt + 1}/${MAX_RETRIES} (${resp.status}) in ${delay.toFixed(0)}ms`);
|
|
377
247
|
await sleep(delay);
|
|
@@ -406,7 +276,7 @@ var DeeplakeApi = class {
|
|
|
406
276
|
`SELECT path FROM "${this.tableName}" WHERE path = '${sqlStr(row.path)}' LIMIT 1`
|
|
407
277
|
);
|
|
408
278
|
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}'`;
|
|
279
|
+
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
280
|
if (row.project !== void 0) setClauses += `, project = '${sqlStr(row.project)}'`;
|
|
411
281
|
if (row.description !== void 0) setClauses += `, description = '${sqlStr(row.description)}'`;
|
|
412
282
|
await this.query(
|
|
@@ -414,8 +284,8 @@ var DeeplakeApi = class {
|
|
|
414
284
|
);
|
|
415
285
|
} else {
|
|
416
286
|
const id = randomUUID();
|
|
417
|
-
let cols =
|
|
418
|
-
let vals = `'${id}', '${sqlStr(row.path)}', '${sqlStr(row.filename)}', E'${sqlStr(row.contentText)}', '${sqlStr(row.mimeType)}', ${row.sizeBytes}, '${cd}', '${lud}'`;
|
|
287
|
+
let cols = `id, path, filename, summary, ${SUMMARY_EMBEDDING_COL}, mime_type, size_bytes, creation_date, last_update_date`;
|
|
288
|
+
let vals = `'${id}', '${sqlStr(row.path)}', '${sqlStr(row.filename)}', E'${sqlStr(row.contentText)}', NULL, '${sqlStr(row.mimeType)}', ${row.sizeBytes}, '${cd}', '${lud}'`;
|
|
419
289
|
if (row.project !== void 0) {
|
|
420
290
|
cols += ", project";
|
|
421
291
|
vals += `, '${sqlStr(row.project)}'`;
|
|
@@ -444,49 +314,79 @@ var DeeplakeApi = class {
|
|
|
444
314
|
buildLookupIndexName(table, suffix) {
|
|
445
315
|
return `idx_${table}_${suffix}`.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
446
316
|
}
|
|
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
317
|
async ensureLookupIndex(table, suffix, columnsSql) {
|
|
477
|
-
|
|
318
|
+
const markers = await getIndexMarkerStore();
|
|
319
|
+
const markerPath = markers.buildIndexMarkerPath(this.workspaceId, this.orgId, table, suffix);
|
|
320
|
+
if (markers.hasFreshIndexMarker(markerPath)) return;
|
|
478
321
|
const indexName = this.buildLookupIndexName(table, suffix);
|
|
479
322
|
try {
|
|
480
323
|
await this.query(`CREATE INDEX IF NOT EXISTS "${indexName}" ON "${table}" ${columnsSql}`);
|
|
481
|
-
|
|
324
|
+
markers.writeIndexMarker(markerPath);
|
|
482
325
|
} catch (e) {
|
|
483
326
|
if (isDuplicateIndexError(e)) {
|
|
484
|
-
|
|
327
|
+
markers.writeIndexMarker(markerPath);
|
|
485
328
|
return;
|
|
486
329
|
}
|
|
487
330
|
log2(`index "${indexName}" skipped: ${e.message}`);
|
|
488
331
|
}
|
|
489
332
|
}
|
|
333
|
+
/**
|
|
334
|
+
* Ensure a vector column exists on the given table.
|
|
335
|
+
*
|
|
336
|
+
* The previous implementation always issued `ALTER TABLE ADD COLUMN IF NOT
|
|
337
|
+
* EXISTS …` on every SessionStart. On a long-running workspace that's
|
|
338
|
+
* already migrated, every call returns 500 "Column already exists" — noisy
|
|
339
|
+
* in the log and a wasted round-trip. Worse, the very first call after the
|
|
340
|
+
* column is genuinely added triggers Deeplake's post-ALTER `vector::at`
|
|
341
|
+
* window (~30s) during which subsequent INSERTs fail; minimising the
|
|
342
|
+
* number of ALTER calls minimises exposure to that window.
|
|
343
|
+
*
|
|
344
|
+
* New flow:
|
|
345
|
+
* 1. Check the local marker file (mirrors ensureLookupIndex). If fresh,
|
|
346
|
+
* return — zero network calls.
|
|
347
|
+
* 2. SELECT 1 FROM information_schema.columns WHERE table_name = T AND
|
|
348
|
+
* column_name = C. Read-only, idempotent, can't tickle the post-ALTER
|
|
349
|
+
* bug. If the column is present → mark + return.
|
|
350
|
+
* 3. Only if step 2 says the column is missing, fall back to ALTER ADD
|
|
351
|
+
* COLUMN IF NOT EXISTS. Mark on success, also mark if Deeplake reports
|
|
352
|
+
* "already exists" (race: another client added it between our SELECT
|
|
353
|
+
* and ALTER).
|
|
354
|
+
*
|
|
355
|
+
* Marker uses the same dir / TTL as ensureLookupIndex so both schema
|
|
356
|
+
* caches share an opt-out (HIVEMIND_INDEX_MARKER_DIR) and a TTL knob.
|
|
357
|
+
*/
|
|
358
|
+
async ensureEmbeddingColumn(table, column) {
|
|
359
|
+
await this.ensureColumn(table, column, "FLOAT4[]");
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Generic marker-gated column migration. Same SELECT-then-ALTER flow as
|
|
363
|
+
* ensureEmbeddingColumn, parameterized by SQL type so it can patch up any
|
|
364
|
+
* column that was added to the schema after the table was originally
|
|
365
|
+
* created. Used today for `summary_embedding`, `message_embedding`, and
|
|
366
|
+
* the `agent` column (added 2026-04-11) — the latter has no fallback if
|
|
367
|
+
* a user upgraded over a pre-2026-04-11 table, so every INSERT fails
|
|
368
|
+
* with `column "agent" does not exist`.
|
|
369
|
+
*/
|
|
370
|
+
async ensureColumn(table, column, sqlType) {
|
|
371
|
+
const markers = await getIndexMarkerStore();
|
|
372
|
+
const markerPath = markers.buildIndexMarkerPath(this.workspaceId, this.orgId, table, `col_${column}`);
|
|
373
|
+
if (markers.hasFreshIndexMarker(markerPath)) return;
|
|
374
|
+
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`;
|
|
375
|
+
const rows = await this.query(colCheck);
|
|
376
|
+
if (rows.length > 0) {
|
|
377
|
+
markers.writeIndexMarker(markerPath);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
try {
|
|
381
|
+
await this.query(`ALTER TABLE "${table}" ADD COLUMN ${column} ${sqlType}`);
|
|
382
|
+
} catch (e) {
|
|
383
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
384
|
+
if (!/already exists/i.test(msg)) throw e;
|
|
385
|
+
const recheck = await this.query(colCheck);
|
|
386
|
+
if (recheck.length === 0) throw e;
|
|
387
|
+
}
|
|
388
|
+
markers.writeIndexMarker(markerPath);
|
|
389
|
+
}
|
|
490
390
|
/** List all tables in the workspace (with retry). */
|
|
491
391
|
async listTables(forceRefresh = false) {
|
|
492
392
|
if (!forceRefresh && this._tablesCache) return [...this._tablesCache];
|
|
@@ -500,7 +400,8 @@ var DeeplakeApi = class {
|
|
|
500
400
|
const resp = await fetch(`${this.apiUrl}/workspaces/${this.workspaceId}/tables`, {
|
|
501
401
|
headers: {
|
|
502
402
|
Authorization: `Bearer ${this.token}`,
|
|
503
|
-
"X-Activeloop-Org-Id": this.orgId
|
|
403
|
+
"X-Activeloop-Org-Id": this.orgId,
|
|
404
|
+
...deeplakeClientHeader()
|
|
504
405
|
}
|
|
505
406
|
});
|
|
506
407
|
if (resp.ok) {
|
|
@@ -525,31 +426,90 @@ var DeeplakeApi = class {
|
|
|
525
426
|
}
|
|
526
427
|
return { tables: [], cacheable: false };
|
|
527
428
|
}
|
|
429
|
+
/**
|
|
430
|
+
* Run a `CREATE TABLE` with an extra outer retry budget. The base
|
|
431
|
+
* `query()` already retries 3 times on fetch errors (~3.5s total), but a
|
|
432
|
+
* failed CREATE is permanent corruption — every subsequent SELECT against
|
|
433
|
+
* the missing table fails. Wrapping in an outer loop with longer backoff
|
|
434
|
+
* (2s, 5s, then 10s) gives us ~17s of reach across transient network
|
|
435
|
+
* blips before giving up. Failures still propagate; getApi() resets its
|
|
436
|
+
* cache on init failure (openclaw plugin) so the next call retries the
|
|
437
|
+
* whole init flow.
|
|
438
|
+
*/
|
|
439
|
+
async createTableWithRetry(sql, label) {
|
|
440
|
+
const OUTER_BACKOFFS_MS = [2e3, 5e3, 1e4];
|
|
441
|
+
let lastErr = null;
|
|
442
|
+
for (let attempt = 0; attempt <= OUTER_BACKOFFS_MS.length; attempt++) {
|
|
443
|
+
try {
|
|
444
|
+
await this.query(sql);
|
|
445
|
+
return;
|
|
446
|
+
} catch (err) {
|
|
447
|
+
lastErr = err;
|
|
448
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
449
|
+
log2(`CREATE TABLE "${label}" attempt ${attempt + 1}/${OUTER_BACKOFFS_MS.length + 1} failed: ${msg}`);
|
|
450
|
+
if (attempt < OUTER_BACKOFFS_MS.length) {
|
|
451
|
+
await sleep(OUTER_BACKOFFS_MS[attempt]);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
throw lastErr;
|
|
456
|
+
}
|
|
528
457
|
/** Create the memory table if it doesn't already exist. Migrate columns on existing tables. */
|
|
529
458
|
async ensureTable(name) {
|
|
530
|
-
const tbl = name ?? this.tableName;
|
|
459
|
+
const tbl = sqlIdent(name ?? this.tableName);
|
|
531
460
|
const tables = await this.listTables();
|
|
532
461
|
if (!tables.includes(tbl)) {
|
|
533
462
|
log2(`table "${tbl}" not found, creating`);
|
|
534
|
-
await this.
|
|
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
|
|
463
|
+
await this.createTableWithRetry(
|
|
464
|
+
`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`,
|
|
465
|
+
tbl
|
|
536
466
|
);
|
|
537
467
|
log2(`table "${tbl}" created`);
|
|
538
468
|
if (!tables.includes(tbl)) this._tablesCache = [...tables, tbl];
|
|
539
469
|
}
|
|
470
|
+
await this.ensureEmbeddingColumn(tbl, SUMMARY_EMBEDDING_COL);
|
|
471
|
+
await this.ensureColumn(tbl, "agent", "TEXT NOT NULL DEFAULT ''");
|
|
540
472
|
}
|
|
541
473
|
/** Create the sessions table (uses JSONB for message since every row is a JSON event). */
|
|
542
474
|
async ensureSessionsTable(name) {
|
|
475
|
+
const safe = sqlIdent(name);
|
|
543
476
|
const tables = await this.listTables();
|
|
544
|
-
if (!tables.includes(
|
|
545
|
-
log2(`table "${
|
|
546
|
-
await this.
|
|
547
|
-
`CREATE TABLE IF NOT EXISTS "${
|
|
477
|
+
if (!tables.includes(safe)) {
|
|
478
|
+
log2(`table "${safe}" not found, creating`);
|
|
479
|
+
await this.createTableWithRetry(
|
|
480
|
+
`CREATE TABLE IF NOT EXISTS "${safe}" (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`,
|
|
481
|
+
safe
|
|
548
482
|
);
|
|
549
|
-
log2(`table "${
|
|
550
|
-
if (!tables.includes(
|
|
483
|
+
log2(`table "${safe}" created`);
|
|
484
|
+
if (!tables.includes(safe)) this._tablesCache = [...tables, safe];
|
|
551
485
|
}
|
|
552
|
-
await this.
|
|
486
|
+
await this.ensureEmbeddingColumn(safe, MESSAGE_EMBEDDING_COL);
|
|
487
|
+
await this.ensureColumn(safe, "agent", "TEXT NOT NULL DEFAULT ''");
|
|
488
|
+
await this.ensureLookupIndex(safe, "path_creation_date", `("path", "creation_date")`);
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Create the skills table.
|
|
492
|
+
*
|
|
493
|
+
* One row per skill version. Workers INSERT a fresh row on every KEEP /
|
|
494
|
+
* MERGE rather than UPDATE-ing in place, so the full version history is
|
|
495
|
+
* recoverable. Uniqueness in the *current* state is by (project_key, name)
|
|
496
|
+
* — newer rows shadow older ones at read time (ORDER BY version DESC).
|
|
497
|
+
* This sidesteps the Deeplake UPDATE-coalescing quirk that bit the wiki
|
|
498
|
+
* worker.
|
|
499
|
+
*/
|
|
500
|
+
async ensureSkillsTable(name) {
|
|
501
|
+
const safe = sqlIdent(name);
|
|
502
|
+
const tables = await this.listTables();
|
|
503
|
+
if (!tables.includes(safe)) {
|
|
504
|
+
log2(`table "${safe}" not found, creating`);
|
|
505
|
+
await this.createTableWithRetry(
|
|
506
|
+
`CREATE TABLE IF NOT EXISTS "${safe}" (id TEXT NOT NULL DEFAULT '', name TEXT NOT NULL DEFAULT '', project TEXT NOT NULL DEFAULT '', project_key TEXT NOT NULL DEFAULT '', local_path TEXT NOT NULL DEFAULT '', install TEXT NOT NULL DEFAULT 'project', source_sessions TEXT NOT NULL DEFAULT '[]', source_agent TEXT NOT NULL DEFAULT '', scope TEXT NOT NULL DEFAULT 'me', author TEXT NOT NULL DEFAULT '', description TEXT NOT NULL DEFAULT '', trigger_text TEXT NOT NULL DEFAULT '', body TEXT NOT NULL DEFAULT '', version BIGINT NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT '', updated_at TEXT NOT NULL DEFAULT '') USING deeplake`,
|
|
507
|
+
safe
|
|
508
|
+
);
|
|
509
|
+
log2(`table "${safe}" created`);
|
|
510
|
+
if (!tables.includes(safe)) this._tablesCache = [...tables, safe];
|
|
511
|
+
}
|
|
512
|
+
await this.ensureLookupIndex(safe, "project_key_name", `("project_key", "name")`);
|
|
553
513
|
}
|
|
554
514
|
};
|
|
555
515
|
|
|
@@ -700,22 +660,25 @@ function normalizeContent(path, raw) {
|
|
|
700
660
|
return raw;
|
|
701
661
|
}
|
|
702
662
|
if (Array.isArray(obj.turns)) {
|
|
703
|
-
const
|
|
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
|
-
}
|
|
663
|
+
const dateHeader = obj.date_time ? `(${String(obj.date_time)}) ` : "";
|
|
710
664
|
const lines = obj.turns.map((t) => {
|
|
711
665
|
const sp = String(t?.speaker ?? t?.name ?? "?").trim();
|
|
712
666
|
const tx = String(t?.text ?? t?.content ?? "").replace(/\s+/g, " ").trim();
|
|
713
667
|
const tag = t?.dia_id ? `[${t.dia_id}] ` : "";
|
|
714
|
-
return `${tag}${sp}: ${tx}`;
|
|
668
|
+
return `${dateHeader}${tag}${sp}: ${tx}`;
|
|
715
669
|
});
|
|
716
|
-
const out2 =
|
|
670
|
+
const out2 = lines.join("\n");
|
|
717
671
|
return out2.trim() ? out2 : raw;
|
|
718
672
|
}
|
|
673
|
+
if (obj.turn && typeof obj.turn === "object" && !Array.isArray(obj.turn)) {
|
|
674
|
+
const t = obj.turn;
|
|
675
|
+
const sp = String(t.speaker ?? t.name ?? "?").trim();
|
|
676
|
+
const tx = String(t.text ?? t.content ?? "").replace(/\s+/g, " ").trim();
|
|
677
|
+
const tag = t.dia_id ? `[${String(t.dia_id)}] ` : "";
|
|
678
|
+
const dateHeader = obj.date_time ? `(${String(obj.date_time)}) ` : "";
|
|
679
|
+
const line = `${dateHeader}${tag}${sp}: ${tx}`;
|
|
680
|
+
return line.trim() ? line : raw;
|
|
681
|
+
}
|
|
719
682
|
const stripRecalled = (t) => {
|
|
720
683
|
const i = t.indexOf("<recalled-memories>");
|
|
721
684
|
if (i === -1) return t;
|
|
@@ -753,8 +716,43 @@ function buildPathCondition(targetPath) {
|
|
|
753
716
|
return `(path = '${sqlStr(clean)}' OR path LIKE '${sqlLike(clean)}/%' ESCAPE '\\')`;
|
|
754
717
|
}
|
|
755
718
|
async function searchDeeplakeTables(api2, memoryTable2, sessionsTable2, opts) {
|
|
756
|
-
const { pathFilter, contentScanOnly, likeOp, escapedPattern, prefilterPattern, prefilterPatterns, multiWordPatterns } = opts;
|
|
719
|
+
const { pathFilter, contentScanOnly, likeOp, escapedPattern, prefilterPattern, prefilterPatterns, queryEmbedding, multiWordPatterns } = opts;
|
|
757
720
|
const limit = opts.limit ?? 100;
|
|
721
|
+
if (queryEmbedding && queryEmbedding.length > 0) {
|
|
722
|
+
const vecLit = serializeFloat4Array(queryEmbedding);
|
|
723
|
+
const semanticLimit = Math.min(
|
|
724
|
+
limit,
|
|
725
|
+
Number(process.env.HIVEMIND_SEMANTIC_LIMIT ?? "20")
|
|
726
|
+
);
|
|
727
|
+
const lexicalLimit = Math.min(
|
|
728
|
+
limit,
|
|
729
|
+
Number(process.env.HIVEMIND_HYBRID_LEXICAL_LIMIT ?? "20")
|
|
730
|
+
);
|
|
731
|
+
const filterPatternsForLex = contentScanOnly ? prefilterPatterns && prefilterPatterns.length > 0 ? prefilterPatterns : prefilterPattern ? [prefilterPattern] : [] : [escapedPattern];
|
|
732
|
+
const memLexFilter = buildContentFilter("summary::text", likeOp, filterPatternsForLex);
|
|
733
|
+
const sessLexFilter = buildContentFilter("message::text", likeOp, filterPatternsForLex);
|
|
734
|
+
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;
|
|
735
|
+
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;
|
|
736
|
+
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}`;
|
|
737
|
+
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}`;
|
|
738
|
+
const parts = [memSemQuery, sessSemQuery];
|
|
739
|
+
if (memLexQuery) parts.push(memLexQuery);
|
|
740
|
+
if (sessLexQuery) parts.push(sessLexQuery);
|
|
741
|
+
const unionSql = parts.map((q) => `(${q})`).join(" UNION ALL ");
|
|
742
|
+
const outerLimit = semanticLimit + lexicalLimit;
|
|
743
|
+
const rows2 = await api2.query(
|
|
744
|
+
`SELECT path, content, source_order, creation_date, score FROM (` + unionSql + `) AS combined ORDER BY score DESC LIMIT ${outerLimit}`
|
|
745
|
+
);
|
|
746
|
+
const seen = /* @__PURE__ */ new Set();
|
|
747
|
+
const unique = [];
|
|
748
|
+
for (const row of rows2) {
|
|
749
|
+
const p = String(row["path"]);
|
|
750
|
+
if (seen.has(p)) continue;
|
|
751
|
+
seen.add(p);
|
|
752
|
+
unique.push({ path: p, content: String(row["content"] ?? "") });
|
|
753
|
+
}
|
|
754
|
+
return unique;
|
|
755
|
+
}
|
|
758
756
|
const filterPatterns = contentScanOnly ? prefilterPatterns && prefilterPatterns.length > 0 ? prefilterPatterns : prefilterPattern ? [prefilterPattern] : [] : multiWordPatterns && multiWordPatterns.length > 1 ? multiWordPatterns : [escapedPattern];
|
|
759
757
|
const memFilter = buildContentFilter("summary::text", likeOp, filterPatterns);
|
|
760
758
|
const sessFilter = buildContentFilter("message::text", likeOp, filterPatterns);
|
|
@@ -768,6 +766,14 @@ async function searchDeeplakeTables(api2, memoryTable2, sessionsTable2, opts) {
|
|
|
768
766
|
content: String(row["content"] ?? "")
|
|
769
767
|
}));
|
|
770
768
|
}
|
|
769
|
+
function serializeFloat4Array(vec) {
|
|
770
|
+
const parts = [];
|
|
771
|
+
for (const v of vec) {
|
|
772
|
+
if (!Number.isFinite(v)) return "NULL";
|
|
773
|
+
parts.push(String(v));
|
|
774
|
+
}
|
|
775
|
+
return `ARRAY[${parts.join(",")}]::float4[]`;
|
|
776
|
+
}
|
|
771
777
|
function buildPathFilter(targetPath) {
|
|
772
778
|
const condition = buildPathCondition(targetPath);
|
|
773
779
|
return condition ? ` AND ${condition}` : "";
|
|
@@ -842,7 +848,7 @@ function buildGrepSearchOptions(params, targetPath) {
|
|
|
842
848
|
return {
|
|
843
849
|
pathFilter: buildPathFilter(targetPath),
|
|
844
850
|
contentScanOnly: hasRegexMeta,
|
|
845
|
-
likeOp:
|
|
851
|
+
likeOp: process.env.HIVEMIND_GREP_LIKE === "case-sensitive" ? "LIKE" : "ILIKE",
|
|
846
852
|
escapedPattern: sqlLike(params.pattern),
|
|
847
853
|
prefilterPattern: literalPrefilter ? sqlLike(literalPrefilter) : void 0,
|
|
848
854
|
prefilterPatterns: alternationPrefilters?.map((literal) => sqlLike(literal)),
|
|
@@ -871,33 +877,64 @@ function compileGrepRegex(params) {
|
|
|
871
877
|
function normalizeSessionPart(path, content) {
|
|
872
878
|
return normalizeContent(path, content);
|
|
873
879
|
}
|
|
874
|
-
|
|
875
|
-
|
|
880
|
+
var INDEX_LIMIT_PER_SECTION = 50;
|
|
881
|
+
function buildVirtualIndexContent(summaryRows, sessionRows = [], opts = {}) {
|
|
876
882
|
const lines = [
|
|
877
|
-
"#
|
|
883
|
+
"# Session Index",
|
|
878
884
|
"",
|
|
879
|
-
|
|
885
|
+
"Two sources are available. Consult the section relevant to the question.",
|
|
880
886
|
""
|
|
881
887
|
];
|
|
882
|
-
|
|
883
|
-
|
|
888
|
+
lines.push("## memory", "");
|
|
889
|
+
if (summaryRows.length === 0) {
|
|
890
|
+
lines.push("_(empty \u2014 no summaries ingested yet)_");
|
|
891
|
+
} else {
|
|
892
|
+
lines.push("AI-generated summaries per session. Read these first for topic-level overviews.");
|
|
893
|
+
lines.push("");
|
|
894
|
+
if (opts.summaryTruncated) {
|
|
895
|
+
lines.push(`_Showing ${INDEX_LIMIT_PER_SECTION} most-recent of many \u2014 older summaries reachable via \`Grep pattern="..." path="~/.deeplake/memory"\`._`);
|
|
896
|
+
lines.push("");
|
|
897
|
+
}
|
|
898
|
+
lines.push("| Session | Created | Last Updated | Project | Description |");
|
|
899
|
+
lines.push("|---------|---------|--------------|---------|-------------|");
|
|
884
900
|
for (const row of summaryRows) {
|
|
885
|
-
const
|
|
901
|
+
const p = row["path"] || "";
|
|
902
|
+
const match = p.match(/\/summaries\/([^/]+)\/([^/]+)\.md$/);
|
|
903
|
+
if (!match) continue;
|
|
904
|
+
const summaryUser = match[1];
|
|
905
|
+
const sessionId = match[2];
|
|
906
|
+
const relPath = `summaries/${summaryUser}/${sessionId}.md`;
|
|
886
907
|
const project = row["project"] || "";
|
|
887
|
-
const description =
|
|
888
|
-
const
|
|
889
|
-
|
|
908
|
+
const description = row["description"] || "";
|
|
909
|
+
const creationDate = row["creation_date"] || "";
|
|
910
|
+
const lastUpdateDate = row["last_update_date"] || "";
|
|
911
|
+
lines.push(`| [${sessionId}](${relPath}) | ${creationDate} | ${lastUpdateDate} | ${project} | ${description} |`);
|
|
890
912
|
}
|
|
891
|
-
lines.push("");
|
|
892
913
|
}
|
|
893
|
-
|
|
894
|
-
|
|
914
|
+
lines.push("");
|
|
915
|
+
lines.push("## sessions", "");
|
|
916
|
+
if (sessionRows.length === 0) {
|
|
917
|
+
lines.push("_(empty \u2014 no session records ingested yet)_");
|
|
918
|
+
} else {
|
|
919
|
+
lines.push("Raw session records (dialogue, tool calls). Read for exact detail / quotes.");
|
|
920
|
+
lines.push("");
|
|
921
|
+
if (opts.sessionTruncated) {
|
|
922
|
+
lines.push(`_Showing ${INDEX_LIMIT_PER_SECTION} most-recent of many \u2014 older sessions reachable via \`Grep pattern="..." path="~/.deeplake/memory"\`._`);
|
|
923
|
+
lines.push("");
|
|
924
|
+
}
|
|
925
|
+
lines.push("| Session | Created | Last Updated | Description |");
|
|
926
|
+
lines.push("|---------|---------|--------------|-------------|");
|
|
895
927
|
for (const row of sessionRows) {
|
|
896
|
-
const
|
|
897
|
-
const
|
|
898
|
-
|
|
928
|
+
const p = row["path"] || "";
|
|
929
|
+
const rel = p.startsWith("/") ? p.slice(1) : p;
|
|
930
|
+
const filename = p.split("/").pop() ?? p;
|
|
931
|
+
const description = row["description"] || "";
|
|
932
|
+
const creationDate = row["creation_date"] || "";
|
|
933
|
+
const lastUpdateDate = row["last_update_date"] || "";
|
|
934
|
+
lines.push(`| [${filename}](${rel}) | ${creationDate} | ${lastUpdateDate} | ${description} |`);
|
|
899
935
|
}
|
|
900
936
|
}
|
|
937
|
+
lines.push("");
|
|
901
938
|
return lines.join("\n");
|
|
902
939
|
}
|
|
903
940
|
function buildUnionQuery(memoryQuery, sessionsQuery) {
|
|
@@ -954,15 +991,25 @@ async function readVirtualPathContents(api2, memoryTable2, sessionsTable2, virtu
|
|
|
954
991
|
}
|
|
955
992
|
}
|
|
956
993
|
if (result.get("/index.md") === null && uniquePaths.includes("/index.md")) {
|
|
994
|
+
const fetchLimit = INDEX_LIMIT_PER_SECTION + 1;
|
|
957
995
|
const [summaryRows, sessionRows] = await Promise.all([
|
|
958
996
|
api2.query(
|
|
959
|
-
`SELECT path, project, description, creation_date FROM "${memoryTable2}" WHERE path LIKE '/summaries/%' ORDER BY
|
|
997
|
+
`SELECT path, project, description, creation_date, last_update_date FROM "${memoryTable2}" WHERE path LIKE '/summaries/%' ORDER BY last_update_date DESC LIMIT ${fetchLimit}`
|
|
960
998
|
).catch(() => []),
|
|
961
999
|
api2.query(
|
|
962
|
-
`SELECT path, description FROM "${sessionsTable2}" WHERE path LIKE '/sessions/%' ORDER BY
|
|
1000
|
+
`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
1001
|
).catch(() => [])
|
|
964
1002
|
]);
|
|
965
|
-
|
|
1003
|
+
const summaryTruncated = summaryRows.length > INDEX_LIMIT_PER_SECTION;
|
|
1004
|
+
const sessionTruncated = sessionRows.length > INDEX_LIMIT_PER_SECTION;
|
|
1005
|
+
result.set(
|
|
1006
|
+
"/index.md",
|
|
1007
|
+
buildVirtualIndexContent(
|
|
1008
|
+
summaryRows.slice(0, INDEX_LIMIT_PER_SECTION),
|
|
1009
|
+
sessionRows.slice(0, INDEX_LIMIT_PER_SECTION),
|
|
1010
|
+
{ summaryTruncated, sessionTruncated }
|
|
1011
|
+
)
|
|
1012
|
+
);
|
|
966
1013
|
}
|
|
967
1014
|
return result;
|
|
968
1015
|
}
|
|
@@ -971,9 +1018,50 @@ async function readVirtualPathContent(api2, memoryTable2, sessionsTable2, virtua
|
|
|
971
1018
|
}
|
|
972
1019
|
|
|
973
1020
|
// openclaw/src/index.ts
|
|
1021
|
+
import { fileURLToPath } from "node:url";
|
|
1022
|
+
import { join as joinPath, dirname as dirnamePath } from "node:path";
|
|
1023
|
+
import { homedir as homedir2, tmpdir } from "node:os";
|
|
1024
|
+
import {
|
|
1025
|
+
existsSync as fsExists,
|
|
1026
|
+
mkdirSync as fsMkdir,
|
|
1027
|
+
openSync as fsOpen,
|
|
1028
|
+
closeSync as fsClose,
|
|
1029
|
+
writeFileSync as fsWriteFile,
|
|
1030
|
+
constants as fsConstants
|
|
1031
|
+
} from "node:fs";
|
|
1032
|
+
import { createHash } from "node:crypto";
|
|
1033
|
+
import { createRequire } from "node:module";
|
|
974
1034
|
function definePluginEntry(entry) {
|
|
975
1035
|
return entry;
|
|
976
1036
|
}
|
|
1037
|
+
function loadSetupConfig() {
|
|
1038
|
+
return import("./chunks/setup-config-C35UK4LP.js");
|
|
1039
|
+
}
|
|
1040
|
+
var credsModulePromise = null;
|
|
1041
|
+
var configModulePromise = null;
|
|
1042
|
+
function loadCredsModule() {
|
|
1043
|
+
if (!credsModulePromise) credsModulePromise = import("./chunks/auth-creds-AEKS6D3P.js");
|
|
1044
|
+
return credsModulePromise;
|
|
1045
|
+
}
|
|
1046
|
+
function loadConfigModule() {
|
|
1047
|
+
if (!configModulePromise) configModulePromise = import("./chunks/config-ZLH6JFJS.js");
|
|
1048
|
+
return configModulePromise;
|
|
1049
|
+
}
|
|
1050
|
+
async function loadCredentials2() {
|
|
1051
|
+
const m = await loadCredsModule();
|
|
1052
|
+
return m.loadCredentials();
|
|
1053
|
+
}
|
|
1054
|
+
async function saveCredentials2(creds) {
|
|
1055
|
+
if (!creds) return;
|
|
1056
|
+
const m = await loadCredsModule();
|
|
1057
|
+
m.saveCredentials(creds);
|
|
1058
|
+
}
|
|
1059
|
+
async function loadConfig() {
|
|
1060
|
+
const m = await loadConfigModule();
|
|
1061
|
+
return m.loadConfig();
|
|
1062
|
+
}
|
|
1063
|
+
var requireFromOpenclaw = createRequire(import.meta.url);
|
|
1064
|
+
var { spawn: realSpawn, execFileSync: realExecFileSync } = requireFromOpenclaw("node:child_process");
|
|
977
1065
|
var DEFAULT_API_URL2 = "https://api.deeplake.ai";
|
|
978
1066
|
var VERSION_URL = "https://clawhub.ai/api/v1/packages/hivemind";
|
|
979
1067
|
function extractLatestVersion(body) {
|
|
@@ -984,7 +1072,7 @@ function extractLatestVersion(body) {
|
|
|
984
1072
|
return typeof v === "string" && v.length > 0 ? v : null;
|
|
985
1073
|
}
|
|
986
1074
|
function getInstalledVersion() {
|
|
987
|
-
return "0.
|
|
1075
|
+
return "0.7.9".length > 0 ? "0.7.9" : null;
|
|
988
1076
|
}
|
|
989
1077
|
function isNewer(latest, current) {
|
|
990
1078
|
const parse = (v) => v.replace(/-.*$/, "").split(".").map(Number);
|
|
@@ -1048,7 +1136,8 @@ async function requestAuth() {
|
|
|
1048
1136
|
headers: {
|
|
1049
1137
|
Authorization: `Bearer ${token}`,
|
|
1050
1138
|
"Content-Type": "application/json",
|
|
1051
|
-
"X-Activeloop-Org-Id": orgId
|
|
1139
|
+
"X-Activeloop-Org-Id": orgId,
|
|
1140
|
+
...deeplakeClientHeader()
|
|
1052
1141
|
},
|
|
1053
1142
|
body: JSON.stringify({ name: `hivemind-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`, duration: 365 * 24 * 60 * 60, organization_id: orgId })
|
|
1054
1143
|
});
|
|
@@ -1059,7 +1148,7 @@ async function requestAuth() {
|
|
|
1059
1148
|
} catch {
|
|
1060
1149
|
}
|
|
1061
1150
|
}
|
|
1062
|
-
|
|
1151
|
+
await saveCredentials2({ token: savedToken, orgId, orgName, userName, apiUrl: DEFAULT_API_URL2, savedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1063
1152
|
authPending = false;
|
|
1064
1153
|
authUrl = null;
|
|
1065
1154
|
justAuthenticated = true;
|
|
@@ -1080,9 +1169,112 @@ async function requestAuth() {
|
|
|
1080
1169
|
var api = null;
|
|
1081
1170
|
var sessionsTable = "sessions";
|
|
1082
1171
|
var memoryTable = "memory";
|
|
1172
|
+
var skillsTable = "skills";
|
|
1083
1173
|
var captureEnabled = true;
|
|
1084
1174
|
var capturedCounts = /* @__PURE__ */ new Map();
|
|
1085
1175
|
var fallbackSessionId = crypto.randomUUID();
|
|
1176
|
+
var __openclaw_filename = fileURLToPath(import.meta.url);
|
|
1177
|
+
var __openclaw_dirname = dirnamePath(__openclaw_filename);
|
|
1178
|
+
var OPENCLAW_SKILIFY_WORKER_PATH = joinPath(__openclaw_dirname, "skilify-worker.js");
|
|
1179
|
+
var OPENCLAW_SKILIFY_STATE_DIR = joinPath(homedir2(), ".deeplake", "state", "skilify");
|
|
1180
|
+
function deriveOpenclawProjectKey(channel) {
|
|
1181
|
+
const project = channel || "openclaw";
|
|
1182
|
+
const key = createHash("sha1").update(project).digest("hex").slice(0, 16);
|
|
1183
|
+
return { key, project };
|
|
1184
|
+
}
|
|
1185
|
+
function tryAcquireOpenclawSkilifyLock(projectKey) {
|
|
1186
|
+
try {
|
|
1187
|
+
fsMkdir(OPENCLAW_SKILIFY_STATE_DIR, { recursive: true });
|
|
1188
|
+
const lockPath = joinPath(OPENCLAW_SKILIFY_STATE_DIR, `${projectKey}.worker.lock`);
|
|
1189
|
+
const fd = fsOpen(lockPath, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY);
|
|
1190
|
+
fsClose(fd);
|
|
1191
|
+
return true;
|
|
1192
|
+
} catch {
|
|
1193
|
+
return false;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
function detectOpenclawGateAgent() {
|
|
1197
|
+
const candidates = [
|
|
1198
|
+
["claude_code", "claude"],
|
|
1199
|
+
["codex", "codex"],
|
|
1200
|
+
["cursor", "cursor-agent"],
|
|
1201
|
+
["hermes", "hermes"],
|
|
1202
|
+
["pi", "pi"]
|
|
1203
|
+
];
|
|
1204
|
+
for (const [agent, bin] of candidates) {
|
|
1205
|
+
try {
|
|
1206
|
+
realExecFileSync("which", [bin], { stdio: ["ignore", "pipe", "ignore"] });
|
|
1207
|
+
return agent;
|
|
1208
|
+
} catch {
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
return null;
|
|
1212
|
+
}
|
|
1213
|
+
function spawnOpenclawSkilifyWorker(a) {
|
|
1214
|
+
if (!fsExists(OPENCLAW_SKILIFY_WORKER_PATH)) {
|
|
1215
|
+
a.loggerWarn?.(`skilify worker missing at ${OPENCLAW_SKILIFY_WORKER_PATH} \u2014 reinstall openclaw plugin`);
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
const gateAgent = detectOpenclawGateAgent();
|
|
1219
|
+
if (!gateAgent) {
|
|
1220
|
+
a.loggerWarn?.(`skilify spawn: no delegate gate CLI found on PATH (need one of: claude, codex, cursor-agent, hermes, pi). Mining skipped.`);
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
const { key: projectKey, project } = deriveOpenclawProjectKey(a.channel);
|
|
1224
|
+
if (!tryAcquireOpenclawSkilifyLock(projectKey)) {
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
const tmpDir = joinPath(tmpdir(), `deeplake-skilify-openclaw-${projectKey}-${Date.now()}`);
|
|
1228
|
+
try {
|
|
1229
|
+
fsMkdir(tmpDir, { recursive: true, mode: 448 });
|
|
1230
|
+
} catch (e) {
|
|
1231
|
+
a.loggerWarn?.(`skilify spawn: mkdir failed: ${e?.message ?? e}`);
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
const configPath = joinPath(tmpDir, "config.json");
|
|
1235
|
+
const config = {
|
|
1236
|
+
apiUrl: a.apiUrl,
|
|
1237
|
+
token: a.token,
|
|
1238
|
+
orgId: a.orgId,
|
|
1239
|
+
workspaceId: a.workspaceId,
|
|
1240
|
+
sessionsTable,
|
|
1241
|
+
skillsTable,
|
|
1242
|
+
userName: a.userName,
|
|
1243
|
+
cwd: homedir2(),
|
|
1244
|
+
// sentinel — only used by worker if install=project
|
|
1245
|
+
projectKey,
|
|
1246
|
+
project,
|
|
1247
|
+
agent: "openclaw",
|
|
1248
|
+
gateAgent,
|
|
1249
|
+
// delegate CLI for the worker's gate call (openclaw has no CLI of its own)
|
|
1250
|
+
scope: "me",
|
|
1251
|
+
team: [],
|
|
1252
|
+
install: "global",
|
|
1253
|
+
tmpDir,
|
|
1254
|
+
gateBin: null,
|
|
1255
|
+
// worker uses gateAgent to look up the binary itself
|
|
1256
|
+
cursorModel: void 0,
|
|
1257
|
+
hermesProvider: void 0,
|
|
1258
|
+
hermesModel: void 0,
|
|
1259
|
+
skilifyLog: joinPath(homedir2(), ".deeplake", "hivemind-openclaw-skilify.log"),
|
|
1260
|
+
currentSessionId: a.sessionId
|
|
1261
|
+
};
|
|
1262
|
+
try {
|
|
1263
|
+
fsWriteFile(configPath, JSON.stringify(config), { mode: 384 });
|
|
1264
|
+
} catch (e) {
|
|
1265
|
+
a.loggerWarn?.(`skilify spawn: config write failed: ${e?.message ?? e}`);
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
try {
|
|
1269
|
+
realSpawn(process.execPath, [OPENCLAW_SKILIFY_WORKER_PATH, configPath], {
|
|
1270
|
+
detached: true,
|
|
1271
|
+
stdio: "ignore",
|
|
1272
|
+
env: { ...process.env, HIVEMIND_SKILIFY_WORKER: "1", HIVEMIND_CAPTURE: "false" }
|
|
1273
|
+
}).unref();
|
|
1274
|
+
} catch (e) {
|
|
1275
|
+
a.loggerWarn?.(`skilify spawn: spawn failed: ${e?.message ?? e}`);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1086
1278
|
function buildSessionPath(config, sessionId) {
|
|
1087
1279
|
return `/sessions/${config.userName}/${config.userName}_${config.orgName}_${config.workspaceId}_${sessionId}.jsonl`;
|
|
1088
1280
|
}
|
|
@@ -1150,16 +1342,18 @@ function normalizeVirtualPath(p) {
|
|
|
1150
1342
|
}
|
|
1151
1343
|
async function getApi() {
|
|
1152
1344
|
if (api) return api;
|
|
1153
|
-
const config = loadConfig();
|
|
1345
|
+
const config = await loadConfig();
|
|
1154
1346
|
if (!config) {
|
|
1155
1347
|
if (!authPending) await requestAuth();
|
|
1156
1348
|
return null;
|
|
1157
1349
|
}
|
|
1158
1350
|
sessionsTable = config.sessionsTableName;
|
|
1159
1351
|
memoryTable = config.tableName;
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
await
|
|
1352
|
+
skillsTable = config.skillsTableName;
|
|
1353
|
+
const candidate = new DeeplakeApi(config.token, config.apiUrl, config.orgId, config.workspaceId, config.tableName);
|
|
1354
|
+
await candidate.ensureTable();
|
|
1355
|
+
await candidate.ensureSessionsTable(sessionsTable);
|
|
1356
|
+
api = candidate;
|
|
1163
1357
|
return api;
|
|
1164
1358
|
}
|
|
1165
1359
|
var src_default = definePluginEntry({
|
|
@@ -1167,432 +1361,442 @@ var src_default = definePluginEntry({
|
|
|
1167
1361
|
name: "Hivemind",
|
|
1168
1362
|
description: "Cloud-backed shared memory powered by Deeplake",
|
|
1169
1363
|
register(pluginApi) {
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1364
|
+
void (async () => {
|
|
1365
|
+
try {
|
|
1366
|
+
pluginApi.registerCommand({
|
|
1367
|
+
name: "hivemind_login",
|
|
1368
|
+
description: "Log in to Hivemind (or switch accounts)",
|
|
1369
|
+
handler: async () => {
|
|
1370
|
+
const existing = await loadCredentials2();
|
|
1371
|
+
const url = await requestAuth();
|
|
1372
|
+
if (existing?.token) {
|
|
1373
|
+
return {
|
|
1374
|
+
text: `\u2139\uFE0F Currently logged in as ${existing.orgName ?? existing.orgId}.
|
|
1180
1375
|
|
|
1181
1376
|
To re-authenticate or switch accounts:
|
|
1182
1377
|
|
|
1183
1378
|
${url}
|
|
1184
1379
|
|
|
1185
1380
|
After signing in, send another message.`
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
return { text: `\u{1F510} Sign in to activate Hivemind memory:
|
|
1189
1384
|
|
|
1190
1385
|
${url}
|
|
1191
1386
|
|
|
1192
1387
|
After signing in, send another message.` };
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1388
|
+
}
|
|
1389
|
+
});
|
|
1390
|
+
pluginApi.registerCommand({
|
|
1391
|
+
name: "hivemind_capture",
|
|
1392
|
+
description: "Toggle conversation capture on/off",
|
|
1393
|
+
handler: async () => {
|
|
1394
|
+
captureEnabled = !captureEnabled;
|
|
1395
|
+
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." };
|
|
1396
|
+
}
|
|
1397
|
+
});
|
|
1398
|
+
pluginApi.registerCommand({
|
|
1399
|
+
name: "hivemind_whoami",
|
|
1400
|
+
description: "Show current Hivemind org and workspace",
|
|
1401
|
+
handler: async () => {
|
|
1402
|
+
const creds2 = await loadCredentials2();
|
|
1403
|
+
if (!creds2?.token) return { text: "Not logged in. Run /hivemind_login" };
|
|
1404
|
+
return { text: `Org: ${creds2.orgName ?? creds2.orgId}
|
|
1210
1405
|
Workspace: ${creds2.workspaceId ?? "default"}` };
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1406
|
+
}
|
|
1407
|
+
});
|
|
1408
|
+
pluginApi.registerCommand({
|
|
1409
|
+
name: "hivemind_orgs",
|
|
1410
|
+
description: "List available organizations",
|
|
1411
|
+
handler: async () => {
|
|
1412
|
+
const creds2 = await loadCredentials2();
|
|
1413
|
+
if (!creds2?.token) return { text: "Not logged in. Run /hivemind_login" };
|
|
1414
|
+
const orgs = await listOrgs(creds2.token, creds2.apiUrl);
|
|
1415
|
+
if (!orgs.length) return { text: "No organizations found." };
|
|
1416
|
+
const lines = orgs.map((o) => `${o.id === creds2.orgId ? "\u2192 " : " "}${o.name}`);
|
|
1417
|
+
return { text: lines.join("\n") };
|
|
1418
|
+
}
|
|
1419
|
+
});
|
|
1420
|
+
pluginApi.registerCommand({
|
|
1421
|
+
name: "hivemind_switch_org",
|
|
1422
|
+
description: "Switch to a different organization",
|
|
1423
|
+
acceptsArgs: true,
|
|
1424
|
+
handler: async (ctx) => {
|
|
1425
|
+
const creds2 = await loadCredentials2();
|
|
1426
|
+
if (!creds2?.token) return { text: "Not logged in. Run /hivemind_login" };
|
|
1427
|
+
const target = ctx.args?.trim();
|
|
1428
|
+
if (!target) return { text: "Usage: /hivemind_switch_org <name-or-id>" };
|
|
1429
|
+
const orgs = await listOrgs(creds2.token, creds2.apiUrl);
|
|
1430
|
+
const lc = target.toLowerCase();
|
|
1431
|
+
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));
|
|
1432
|
+
if (!match) {
|
|
1433
|
+
const available = orgs.length ? orgs.map((o) => ` - ${o.name} (id: ${o.id})`).join("\n") : " (none \u2014 your current token has no organization access)";
|
|
1434
|
+
return { text: `Org not found: ${target}
|
|
1240
1435
|
|
|
1241
1436
|
Available:
|
|
1242
1437
|
${available}` };
|
|
1438
|
+
}
|
|
1439
|
+
await switchOrg(match.id, match.name);
|
|
1440
|
+
api = null;
|
|
1441
|
+
return { text: `Switched to org: ${match.name}` };
|
|
1243
1442
|
}
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
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}
|
|
1443
|
+
});
|
|
1444
|
+
pluginApi.registerCommand({
|
|
1445
|
+
name: "hivemind_workspaces",
|
|
1446
|
+
description: "List available workspaces",
|
|
1447
|
+
handler: async () => {
|
|
1448
|
+
const creds2 = await loadCredentials2();
|
|
1449
|
+
if (!creds2?.token) return { text: "Not logged in. Run /hivemind_login" };
|
|
1450
|
+
const ws = await listWorkspaces(creds2.token, creds2.apiUrl, creds2.orgId);
|
|
1451
|
+
if (!ws.length) return { text: "No workspaces found." };
|
|
1452
|
+
const lines = ws.map((w) => `${w.id === (creds2.workspaceId ?? "default") ? "\u2192 " : " "}${w.name}`);
|
|
1453
|
+
return { text: lines.join("\n") };
|
|
1454
|
+
}
|
|
1455
|
+
});
|
|
1456
|
+
pluginApi.registerCommand({
|
|
1457
|
+
name: "hivemind_switch_workspace",
|
|
1458
|
+
description: "Switch to a different workspace",
|
|
1459
|
+
acceptsArgs: true,
|
|
1460
|
+
handler: async (ctx) => {
|
|
1461
|
+
const creds2 = await loadCredentials2();
|
|
1462
|
+
if (!creds2?.token) return { text: "Not logged in. Run /hivemind_login" };
|
|
1463
|
+
const target = ctx.args?.trim();
|
|
1464
|
+
if (!target) return { text: "Usage: /hivemind_switch_workspace <name-or-id>" };
|
|
1465
|
+
const ws = await listWorkspaces(creds2.token, creds2.apiUrl, creds2.orgId);
|
|
1466
|
+
const lc = target.toLowerCase();
|
|
1467
|
+
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));
|
|
1468
|
+
if (!match) {
|
|
1469
|
+
const available = ws.length ? ws.map((w) => ` - ${w.name} (id: ${w.id})`).join("\n") : " (none in current org \u2014 try /hivemind_switch_org first)";
|
|
1470
|
+
return { text: `Workspace not found: ${target}
|
|
1276
1471
|
|
|
1277
1472
|
Available:
|
|
1278
1473
|
${available}` };
|
|
1474
|
+
}
|
|
1475
|
+
await switchWorkspace(match.id);
|
|
1476
|
+
api = null;
|
|
1477
|
+
return { text: `Switched to workspace: ${match.name}` };
|
|
1279
1478
|
}
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
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.
|
|
1479
|
+
});
|
|
1480
|
+
pluginApi.registerCommand({
|
|
1481
|
+
name: "hivemind_setup",
|
|
1482
|
+
description: "Add Hivemind tools to your openclaw allowlist (needed once per install)",
|
|
1483
|
+
handler: async () => {
|
|
1484
|
+
const { ensureHivemindAllowlisted } = await loadSetupConfig();
|
|
1485
|
+
const result = ensureHivemindAllowlisted();
|
|
1486
|
+
const skilifyHint = `
|
|
1292
1487
|
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1488
|
+
Skill mining (skilify) runs in the background after each turn \u2014 your conversations get crystallised into reusable skills automatically. From your terminal:
|
|
1489
|
+
hivemind skilify status \u2014 see what's been mined
|
|
1490
|
+
hivemind skilify pull \u2014 fetch teammates' skills`;
|
|
1491
|
+
if (result.status === "already-set") {
|
|
1492
|
+
return { text: `\u2705 Hivemind tools are already enabled in your allowlist.
|
|
1493
|
+
|
|
1494
|
+
No changes needed \u2014 memory tools are available to the agent.${skilifyHint}` };
|
|
1495
|
+
}
|
|
1496
|
+
if (result.status === "added") {
|
|
1497
|
+
return { text: `\u2705 Added "hivemind" to your tool allowlist.
|
|
1297
1498
|
|
|
1298
1499
|
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
1500
|
|
|
1300
|
-
Backup of previous config: ${result.backupPath}` };
|
|
1301
|
-
|
|
1302
|
-
|
|
1501
|
+
Backup of previous config: ${result.backupPath}${skilifyHint}` };
|
|
1502
|
+
}
|
|
1503
|
+
return { text: `\u26A0\uFE0F Could not update allowlist: ${result.error}
|
|
1303
1504
|
|
|
1304
1505
|
Manual fix: open ${result.configPath} and add "hivemind" to the "alsoAllow" array under "tools".` };
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1506
|
+
}
|
|
1507
|
+
});
|
|
1508
|
+
pluginApi.registerCommand({
|
|
1509
|
+
name: "hivemind_version",
|
|
1510
|
+
description: "Show the installed Hivemind version and check for updates",
|
|
1511
|
+
handler: async () => {
|
|
1512
|
+
const current = getInstalledVersion();
|
|
1513
|
+
if (!current) return { text: "Could not determine installed version." };
|
|
1514
|
+
try {
|
|
1515
|
+
const res = await fetch(VERSION_URL, { signal: AbortSignal.timeout(3e3) });
|
|
1516
|
+
if (!res.ok) return { text: `Current version: ${current}. Could not check for updates.` };
|
|
1517
|
+
const latest = extractLatestVersion(await res.json());
|
|
1518
|
+
if (!latest) return { text: `Current version: ${current}. Could not parse latest version.` };
|
|
1519
|
+
if (isNewer(latest, current)) {
|
|
1520
|
+
return { text: `\u2B06\uFE0F Update available: ${current} \u2192 ${latest}
|
|
1320
1521
|
|
|
1321
1522
|
Run /hivemind_update to install it now.` };
|
|
1523
|
+
}
|
|
1524
|
+
return { text: `\u2705 Hivemind v${current} is up to date.` };
|
|
1525
|
+
} catch {
|
|
1526
|
+
return { text: `Current version: ${current}. Could not check for updates.` };
|
|
1322
1527
|
}
|
|
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
1528
|
}
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
text: `Hivemind v${current} installed. To install the latest:
|
|
1529
|
+
});
|
|
1530
|
+
pluginApi.registerCommand({
|
|
1531
|
+
name: "hivemind_update",
|
|
1532
|
+
description: "Install the latest Hivemind version from ClawHub",
|
|
1533
|
+
handler: async () => {
|
|
1534
|
+
const current = getInstalledVersion() ?? "unknown";
|
|
1535
|
+
return {
|
|
1536
|
+
text: `Hivemind v${current} installed. To install the latest:
|
|
1336
1537
|
|
|
1337
1538
|
\u2022 Ask me in chat: "update hivemind" \u2014 I'll run \`openclaw plugins update hivemind\` via my exec tool.
|
|
1338
1539
|
\u2022 Or run in your terminal: \`openclaw plugins update hivemind\`
|
|
1339
1540
|
|
|
1340
1541
|
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}` };
|
|
1542
|
+
};
|
|
1356
1543
|
}
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
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."
|
|
1544
|
+
});
|
|
1545
|
+
pluginApi.registerCommand({
|
|
1546
|
+
name: "hivemind_autoupdate",
|
|
1547
|
+
description: "Toggle Hivemind auto-update on/off",
|
|
1548
|
+
acceptsArgs: true,
|
|
1549
|
+
handler: async (ctx) => {
|
|
1550
|
+
const arg = ctx.args?.trim().toLowerCase();
|
|
1551
|
+
let setTo;
|
|
1552
|
+
if (arg === "on" || arg === "true" || arg === "enable") setTo = true;
|
|
1553
|
+
else if (arg === "off" || arg === "false" || arg === "disable") setTo = false;
|
|
1554
|
+
const { toggleAutoUpdateConfig } = await loadSetupConfig();
|
|
1555
|
+
const result = toggleAutoUpdateConfig(setTo);
|
|
1556
|
+
if (result.status === "error") {
|
|
1557
|
+
return { text: `\u26A0\uFE0F Could not update auto-update setting: ${result.error}` };
|
|
1392
1558
|
}
|
|
1393
|
-
},
|
|
1394
|
-
required: ["query"]
|
|
1395
|
-
},
|
|
1396
|
-
execute: async (_toolCallId, rawParams) => {
|
|
1397
|
-
const params = rawParams;
|
|
1398
|
-
const dl = await getApi();
|
|
1399
|
-
if (!dl) {
|
|
1400
1559
|
return {
|
|
1401
|
-
|
|
1560
|
+
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
1561
|
};
|
|
1403
1562
|
}
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1563
|
+
});
|
|
1564
|
+
pluginApi.registerTool({
|
|
1565
|
+
name: "hivemind_search",
|
|
1566
|
+
label: "Hivemind Search",
|
|
1567
|
+
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.",
|
|
1568
|
+
parameters: {
|
|
1569
|
+
type: "object",
|
|
1570
|
+
additionalProperties: false,
|
|
1571
|
+
properties: {
|
|
1572
|
+
query: {
|
|
1573
|
+
type: "string",
|
|
1574
|
+
minLength: 1,
|
|
1575
|
+
description: "Search text. Treated as a literal substring by default; set `regex: true` to use regex metacharacters."
|
|
1576
|
+
},
|
|
1577
|
+
path: {
|
|
1578
|
+
type: "string",
|
|
1579
|
+
description: "Optional virtual path prefix to scope the search, e.g. '/summaries/' or '/sessions/alice/'. Defaults to '/' (all of memory)."
|
|
1580
|
+
},
|
|
1581
|
+
regex: {
|
|
1582
|
+
type: "boolean",
|
|
1583
|
+
description: "If true, `query` is interpreted as a regex. Default false (literal substring)."
|
|
1584
|
+
},
|
|
1585
|
+
ignoreCase: {
|
|
1586
|
+
type: "boolean",
|
|
1587
|
+
description: "Case-insensitive match. Default true."
|
|
1588
|
+
},
|
|
1589
|
+
limit: {
|
|
1590
|
+
type: "integer",
|
|
1591
|
+
minimum: 1,
|
|
1592
|
+
maximum: 100,
|
|
1593
|
+
description: "Max rows returned per table. Default 20."
|
|
1594
|
+
}
|
|
1595
|
+
},
|
|
1596
|
+
required: ["query"]
|
|
1597
|
+
},
|
|
1598
|
+
execute: async (_toolCallId, rawParams) => {
|
|
1599
|
+
const params = rawParams;
|
|
1600
|
+
const dl = await getApi();
|
|
1601
|
+
if (!dl) {
|
|
1602
|
+
return {
|
|
1603
|
+
content: [{ type: "text", text: "Not logged in. Run /hivemind_login first." }]
|
|
1604
|
+
};
|
|
1427
1605
|
}
|
|
1428
|
-
const
|
|
1429
|
-
|
|
1430
|
-
|
|
1606
|
+
const targetPath = normalizeVirtualPath(params.path);
|
|
1607
|
+
const grepParams = {
|
|
1608
|
+
pattern: params.query,
|
|
1609
|
+
ignoreCase: params.ignoreCase !== false,
|
|
1610
|
+
wordMatch: false,
|
|
1611
|
+
filesOnly: false,
|
|
1612
|
+
countOnly: false,
|
|
1613
|
+
lineNumber: false,
|
|
1614
|
+
invertMatch: false,
|
|
1615
|
+
fixedString: params.regex !== true
|
|
1616
|
+
};
|
|
1617
|
+
const searchOpts = buildGrepSearchOptions(grepParams, targetPath);
|
|
1618
|
+
searchOpts.limit = Math.min(Math.max(params.limit ?? 20, 1), 100);
|
|
1619
|
+
const t0 = Date.now();
|
|
1620
|
+
try {
|
|
1621
|
+
const rawRows = await searchDeeplakeTables(dl, memoryTable, sessionsTable, searchOpts);
|
|
1622
|
+
const matchedRows = searchOpts.contentScanOnly ? (() => {
|
|
1623
|
+
const re = compileGrepRegex(grepParams);
|
|
1624
|
+
return rawRows.filter((r) => re.test(normalizeContent(r.path, r.content)));
|
|
1625
|
+
})() : rawRows;
|
|
1626
|
+
pluginApi.logger.info?.(`hivemind_search "${params.query.slice(0, 60)}" \u2192 ${matchedRows.length}/${rawRows.length} hits in ${Date.now() - t0}ms`);
|
|
1627
|
+
if (matchedRows.length === 0) {
|
|
1628
|
+
return { content: [{ type: "text", text: `No memory matches for "${params.query}" under ${targetPath}.` }] };
|
|
1629
|
+
}
|
|
1630
|
+
const text = matchedRows.map((r, i) => {
|
|
1631
|
+
const body = normalizeContent(r.path, r.content);
|
|
1632
|
+
return `${i + 1}. ${r.path}
|
|
1431
1633
|
${body.slice(0, 500)}`;
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
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."
|
|
1634
|
+
}).join("\n\n");
|
|
1635
|
+
return { content: [{ type: "text", text }], details: { hits: matchedRows.length, path: targetPath } };
|
|
1636
|
+
} catch (err) {
|
|
1637
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1638
|
+
pluginApi.logger.error(`hivemind_search failed: ${msg}`);
|
|
1639
|
+
return { content: [{ type: "text", text: `Search failed: ${msg}` }] };
|
|
1453
1640
|
}
|
|
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
1641
|
}
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1642
|
+
});
|
|
1643
|
+
pluginApi.registerTool({
|
|
1644
|
+
name: "hivemind_read",
|
|
1645
|
+
label: "Hivemind Read",
|
|
1646
|
+
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.",
|
|
1647
|
+
parameters: {
|
|
1648
|
+
type: "object",
|
|
1649
|
+
additionalProperties: false,
|
|
1650
|
+
properties: {
|
|
1651
|
+
path: {
|
|
1652
|
+
type: "string",
|
|
1653
|
+
minLength: 1,
|
|
1654
|
+
description: "Virtual path under /summaries/, /sessions/, or '/index.md' for the memory index."
|
|
1655
|
+
}
|
|
1656
|
+
},
|
|
1657
|
+
required: ["path"]
|
|
1658
|
+
},
|
|
1659
|
+
execute: async (_toolCallId, rawParams) => {
|
|
1660
|
+
const params = rawParams;
|
|
1661
|
+
const dl = await getApi();
|
|
1662
|
+
if (!dl) {
|
|
1663
|
+
return { content: [{ type: "text", text: "Not logged in. Run /hivemind_login first." }] };
|
|
1664
|
+
}
|
|
1665
|
+
const virtualPath = normalizeVirtualPath(params.path);
|
|
1666
|
+
try {
|
|
1667
|
+
const content = await readVirtualPathContent(dl, memoryTable, sessionsTable, virtualPath);
|
|
1668
|
+
if (content === null) {
|
|
1669
|
+
return { content: [{ type: "text", text: `No content at ${virtualPath}.` }] };
|
|
1670
|
+
}
|
|
1671
|
+
return { content: [{ type: "text", text: content }], details: { path: virtualPath } };
|
|
1672
|
+
} catch (err) {
|
|
1673
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1674
|
+
pluginApi.logger.error(`hivemind_read failed: ${msg}`);
|
|
1675
|
+
return { content: [{ type: "text", text: `Read failed: ${msg}` }] };
|
|
1468
1676
|
}
|
|
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
1677
|
}
|
|
1499
|
-
}
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
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 [];
|
|
1678
|
+
});
|
|
1679
|
+
pluginApi.registerTool({
|
|
1680
|
+
name: "hivemind_index",
|
|
1681
|
+
label: "Hivemind Index",
|
|
1682
|
+
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.",
|
|
1683
|
+
parameters: {
|
|
1684
|
+
type: "object",
|
|
1685
|
+
additionalProperties: false,
|
|
1686
|
+
properties: {}
|
|
1687
|
+
},
|
|
1688
|
+
execute: async () => {
|
|
1689
|
+
const dl = await getApi();
|
|
1690
|
+
if (!dl) {
|
|
1691
|
+
return { content: [{ type: "text", text: "Not logged in. Run /hivemind_login first." }] };
|
|
1692
|
+
}
|
|
1693
|
+
try {
|
|
1694
|
+
const text = await readVirtualPathContent(dl, memoryTable, sessionsTable, "/index.md");
|
|
1695
|
+
return { content: [{ type: "text", text: text ?? "(memory is empty)" }] };
|
|
1696
|
+
} catch (err) {
|
|
1697
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1698
|
+
pluginApi.logger.error(`hivemind_index failed: ${msg}`);
|
|
1699
|
+
return { content: [{ type: "text", text: `Index build failed: ${msg}` }] };
|
|
1700
|
+
}
|
|
1528
1701
|
}
|
|
1529
|
-
}
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
const
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1702
|
+
});
|
|
1703
|
+
pluginApi.registerMemoryCorpusSupplement({
|
|
1704
|
+
search: async ({ query, maxResults }) => {
|
|
1705
|
+
const dl = await getApi();
|
|
1706
|
+
if (!dl) return [];
|
|
1707
|
+
const grepParams = {
|
|
1708
|
+
pattern: query,
|
|
1709
|
+
ignoreCase: true,
|
|
1710
|
+
wordMatch: false,
|
|
1711
|
+
filesOnly: false,
|
|
1712
|
+
countOnly: false,
|
|
1713
|
+
lineNumber: false,
|
|
1714
|
+
invertMatch: false,
|
|
1715
|
+
fixedString: true
|
|
1716
|
+
};
|
|
1717
|
+
const searchOpts = buildGrepSearchOptions(grepParams, "/");
|
|
1718
|
+
searchOpts.limit = Math.min(Math.max(maxResults ?? 10, 1), 50);
|
|
1719
|
+
try {
|
|
1720
|
+
const rows = await searchDeeplakeTables(dl, memoryTable, sessionsTable, searchOpts);
|
|
1721
|
+
return rows.map((r, i) => ({
|
|
1722
|
+
path: r.path,
|
|
1723
|
+
snippet: normalizeContent(r.path, r.content).slice(0, 400),
|
|
1724
|
+
corpus: "hivemind",
|
|
1725
|
+
kind: r.path.startsWith("/summaries/") ? "summary" : "session",
|
|
1726
|
+
score: r.path.startsWith("/summaries/") ? 0.8 - i * 5e-3 : 0.6 - i * 5e-3
|
|
1727
|
+
}));
|
|
1728
|
+
} catch {
|
|
1729
|
+
return [];
|
|
1730
|
+
}
|
|
1731
|
+
},
|
|
1732
|
+
get: async ({ lookup }) => {
|
|
1733
|
+
const dl = await getApi();
|
|
1734
|
+
if (!dl) return null;
|
|
1735
|
+
try {
|
|
1736
|
+
const content = await readVirtualPathContent(dl, memoryTable, sessionsTable, normalizeVirtualPath(lookup));
|
|
1737
|
+
return content === null ? null : { path: lookup, content };
|
|
1738
|
+
} catch {
|
|
1739
|
+
return null;
|
|
1740
|
+
}
|
|
1538
1741
|
}
|
|
1742
|
+
});
|
|
1743
|
+
const config = pluginApi.pluginConfig ?? {};
|
|
1744
|
+
const logger = pluginApi.logger;
|
|
1745
|
+
const hook = (event, handler) => {
|
|
1746
|
+
pluginApi.on(event, handler);
|
|
1747
|
+
};
|
|
1748
|
+
if (config.autoUpdate !== false) {
|
|
1749
|
+
(async () => {
|
|
1750
|
+
try {
|
|
1751
|
+
const current = getInstalledVersion();
|
|
1752
|
+
if (!current) return;
|
|
1753
|
+
const res = await fetch(VERSION_URL, { signal: AbortSignal.timeout(3e3) });
|
|
1754
|
+
if (!res.ok) return;
|
|
1755
|
+
const latest = extractLatestVersion(await res.json());
|
|
1756
|
+
if (!latest || !isNewer(latest, current)) return;
|
|
1757
|
+
pendingUpdate = { current, latest };
|
|
1758
|
+
logger.info?.(`Hivemind update available: ${current} \u2192 ${latest}. Agent will be prompted to install when user asks.`);
|
|
1759
|
+
} catch (err) {
|
|
1760
|
+
logger.error(`Auto-update check failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1761
|
+
}
|
|
1762
|
+
})();
|
|
1539
1763
|
}
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
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 ? `
|
|
1764
|
+
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## Skill Management (skilify)\n\nHivemind also mines reusable Claude skills from agent sessions and stores them in a per-org Deeplake table. Openclaw itself doesn\'t run sessions to mine, but you can pull skills others have already mined for the user. These run in the user\'s terminal (the openclaw plugin does not register them as `/hivemind_*` commands):\n\n- `hivemind skilify` \u2014 show scope/team/install + per-project state\n- `hivemind skilify pull` \u2014 sync skills for the current project from the org table\n- `hivemind skilify pull --user <email>` \u2014 only that author\'s skills\n- `hivemind skilify pull --users a,b,c` \u2014 multiple authors (CSV)\n- `hivemind skilify pull --all-users` \u2014 explicit "no author filter"\n- `hivemind skilify pull --to project|global` \u2014 install location (`<cwd>/.claude/skills/` vs `~/.claude/skills/`)\n- `hivemind skilify pull --dry-run` \u2014 preview without touching disk\n- `hivemind skilify pull --force` \u2014 overwrite local (creates `.bak`)\n- `hivemind skilify pull <skill-name>` \u2014 pull only that one skill (combines with `--user`)\n- `hivemind skilify scope <me|team|org>` \u2014 set sharing scope for new skills\n- `hivemind skilify install <project|global>` \u2014 default install location\n- `hivemind skilify team add|remove|list <name>` \u2014 manage team list\n\nIf the user asks to "pull skills from X", "share skills with the team", or similar, suggest the matching `hivemind skilify` command. Run `hivemind skilify --help` for the full reference.\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) {
|
|
1765
|
+
const setupConfigPromise = loadSetupConfig();
|
|
1766
|
+
hook("before_prompt_build", async () => {
|
|
1767
|
+
const { detectAllowlistMissing } = await setupConfigPromise;
|
|
1768
|
+
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" : "";
|
|
1769
|
+
const updateNudge = pendingUpdate ? `
|
|
1566
1770
|
|
|
1567
1771
|
<hivemind-update-available>
|
|
1568
1772
|
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
1773
|
</hivemind-update-available>
|
|
1570
1774
|
` : "";
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1775
|
+
return {
|
|
1776
|
+
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## Skill Management (skilify)\n\nHivemind also mines reusable Claude skills from agent sessions and stores them in a per-org Deeplake table. Openclaw itself doesn\'t run sessions to mine, but you can pull skills others have already mined for the user. These run in the user\'s terminal (the openclaw plugin does not register them as `/hivemind_*` commands):\n\n- `hivemind skilify` \u2014 show scope/team/install + per-project state\n- `hivemind skilify pull` \u2014 sync skills for the current project from the org table\n- `hivemind skilify pull --user <email>` \u2014 only that author\'s skills\n- `hivemind skilify pull --users a,b,c` \u2014 multiple authors (CSV)\n- `hivemind skilify pull --all-users` \u2014 explicit "no author filter"\n- `hivemind skilify pull --to project|global` \u2014 install location (`<cwd>/.claude/skills/` vs `~/.claude/skills/`)\n- `hivemind skilify pull --dry-run` \u2014 preview without touching disk\n- `hivemind skilify pull --force` \u2014 overwrite local (creates `.bak`)\n- `hivemind skilify pull <skill-name>` \u2014 pull only that one skill (combines with `--user`)\n- `hivemind skilify scope <me|team|org>` \u2014 set sharing scope for new skills\n- `hivemind skilify install <project|global>` \u2014 default install location\n- `hivemind skilify team add|remove|list <name>` \u2014 manage team list\n\nIf the user asks to "pull skills from X", "share skills with the team", or similar, suggest the matching `hivemind skilify` command. Run `hivemind skilify --help` for the full reference.\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'
|
|
1777
|
+
};
|
|
1778
|
+
});
|
|
1779
|
+
}
|
|
1780
|
+
if (config.autoRecall !== false) {
|
|
1781
|
+
hook("before_agent_start", async (event) => {
|
|
1782
|
+
if (!event.prompt || event.prompt.length < 5) return;
|
|
1783
|
+
try {
|
|
1784
|
+
const dl = await getApi();
|
|
1785
|
+
if (!dl && authUrl) {
|
|
1786
|
+
return {
|
|
1787
|
+
prependContext: `
|
|
1584
1788
|
|
|
1585
1789
|
IMPORTANT: The user needs to authenticate with Hivemind. Include this link in your reply: ${authUrl}
|
|
1586
1790
|
Tell them to click it and sign in, then send another message.
|
|
1587
1791
|
`
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1794
|
+
if (!dl) return;
|
|
1795
|
+
if (justAuthenticated) {
|
|
1796
|
+
justAuthenticated = false;
|
|
1797
|
+
const creds2 = await loadCredentials2();
|
|
1798
|
+
const orgName = creds2?.orgName ?? creds2?.orgId ?? "unknown";
|
|
1799
|
+
return { prependContext: `
|
|
1596
1800
|
|
|
1597
1801
|
\u{1F41D} Welcome to Hivemind!
|
|
1598
1802
|
|
|
@@ -1607,106 +1811,121 @@ Get started:
|
|
|
1607
1811
|
|
|
1608
1812
|
One brain for every agent on your team.
|
|
1609
1813
|
` };
|
|
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
1814
|
}
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
const
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1815
|
+
const keywords = extractKeywords(event.prompt);
|
|
1816
|
+
if (!keywords.length) return;
|
|
1817
|
+
const grepParams = {
|
|
1818
|
+
pattern: keywords.join(" "),
|
|
1819
|
+
ignoreCase: true,
|
|
1820
|
+
wordMatch: false,
|
|
1821
|
+
filesOnly: false,
|
|
1822
|
+
countOnly: false,
|
|
1823
|
+
lineNumber: false,
|
|
1824
|
+
invertMatch: false,
|
|
1825
|
+
fixedString: true
|
|
1674
1826
|
};
|
|
1675
|
-
const
|
|
1676
|
-
|
|
1677
|
-
const
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1827
|
+
const searchOpts = buildGrepSearchOptions(grepParams, "/");
|
|
1828
|
+
searchOpts.limit = 10;
|
|
1829
|
+
const rows = await searchDeeplakeTables(dl, memoryTable, sessionsTable, searchOpts);
|
|
1830
|
+
if (!rows.length) return;
|
|
1831
|
+
const recalled = rows.map((r) => {
|
|
1832
|
+
const body = normalizeContent(r.path, r.content);
|
|
1833
|
+
return `[${r.path}] ${body.slice(0, 400)}`;
|
|
1834
|
+
}).join("\n\n");
|
|
1835
|
+
logger.info?.(`Auto-recalled ${rows.length} memories`);
|
|
1836
|
+
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.";
|
|
1837
|
+
return {
|
|
1838
|
+
prependContext: "\n\n<recalled-memories>\n" + instruction + "\n\n" + recalled + "\n</recalled-memories>\n"
|
|
1839
|
+
};
|
|
1840
|
+
} catch (err) {
|
|
1841
|
+
logger.error(`Auto-recall failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1842
|
+
}
|
|
1843
|
+
});
|
|
1844
|
+
}
|
|
1845
|
+
if (config.autoCapture !== false) {
|
|
1846
|
+
hook("agent_end", async (event) => {
|
|
1847
|
+
const ev = event;
|
|
1848
|
+
if (!captureEnabled || !ev.success || !ev.messages?.length) return;
|
|
1849
|
+
try {
|
|
1850
|
+
const dl = await getApi();
|
|
1851
|
+
if (!dl) return;
|
|
1852
|
+
const cfg = await loadConfig();
|
|
1853
|
+
if (!cfg) return;
|
|
1854
|
+
const sid = ev.session_id || fallbackSessionId;
|
|
1855
|
+
const lastCount = capturedCounts.get(sid) ?? 0;
|
|
1856
|
+
const newMessages = ev.messages.slice(lastCount);
|
|
1857
|
+
capturedCounts.set(sid, ev.messages.length);
|
|
1858
|
+
if (!newMessages.length) return;
|
|
1859
|
+
const sessionPath = buildSessionPath(cfg, sid);
|
|
1860
|
+
const filename = sessionPath.split("/").pop() ?? "";
|
|
1861
|
+
const projectName = ev.channel || "openclaw";
|
|
1862
|
+
for (const msg of newMessages) {
|
|
1863
|
+
if (msg.role !== "user" && msg.role !== "assistant") continue;
|
|
1864
|
+
let text = "";
|
|
1865
|
+
if (typeof msg.content === "string") {
|
|
1866
|
+
text = msg.content;
|
|
1867
|
+
} else if (Array.isArray(msg.content)) {
|
|
1868
|
+
text = msg.content.filter((b) => b.type === "text" && b.text).map((b) => b.text).join("\n");
|
|
1869
|
+
}
|
|
1870
|
+
if (!text.trim()) continue;
|
|
1871
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
1872
|
+
const entry = {
|
|
1873
|
+
id: crypto.randomUUID(),
|
|
1874
|
+
type: msg.role === "user" ? "user_message" : "assistant_message",
|
|
1875
|
+
session_id: sid,
|
|
1876
|
+
content: text,
|
|
1877
|
+
timestamp: ts
|
|
1878
|
+
};
|
|
1879
|
+
const line = JSON.stringify(entry);
|
|
1880
|
+
const jsonForSql = line.replace(/'/g, "''");
|
|
1881
|
+
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}')`;
|
|
1882
|
+
try {
|
|
1683
1883
|
await dl.query(insertSql);
|
|
1684
|
-
}
|
|
1685
|
-
|
|
1884
|
+
} catch (e) {
|
|
1885
|
+
if (e.message?.includes("permission denied") || e.message?.includes("does not exist")) {
|
|
1886
|
+
await dl.ensureSessionsTable(sessionsTable);
|
|
1887
|
+
await dl.query(insertSql);
|
|
1888
|
+
} else {
|
|
1889
|
+
throw e;
|
|
1890
|
+
}
|
|
1686
1891
|
}
|
|
1687
1892
|
}
|
|
1893
|
+
logger.info?.(`Auto-captured ${newMessages.length} messages`);
|
|
1894
|
+
try {
|
|
1895
|
+
spawnOpenclawSkilifyWorker({
|
|
1896
|
+
apiUrl: cfg.apiUrl,
|
|
1897
|
+
token: cfg.token,
|
|
1898
|
+
orgId: cfg.orgId,
|
|
1899
|
+
workspaceId: cfg.workspaceId,
|
|
1900
|
+
userName: cfg.userName,
|
|
1901
|
+
channel: ev.channel || "openclaw",
|
|
1902
|
+
sessionId: sid,
|
|
1903
|
+
loggerWarn: (msg) => logger.error(`Skilify spawn: ${msg}`)
|
|
1904
|
+
});
|
|
1905
|
+
} catch (e) {
|
|
1906
|
+
logger.error(`Skilify spawn threw: ${e?.message ?? e}`);
|
|
1907
|
+
}
|
|
1908
|
+
} catch (err) {
|
|
1909
|
+
logger.error(`Auto-capture failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1688
1910
|
}
|
|
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
1911
|
});
|
|
1702
1912
|
}
|
|
1913
|
+
const creds = await loadCredentials2();
|
|
1914
|
+
if (!creds?.token) {
|
|
1915
|
+
logger.info?.("Hivemind installed. Run /hivemind_login to authenticate and activate shared memory.");
|
|
1916
|
+
if (!authPending) {
|
|
1917
|
+
requestAuth().catch((err) => {
|
|
1918
|
+
logger.error(`Pre-auth failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1919
|
+
});
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
checkForUpdate(logger).catch(() => {
|
|
1923
|
+
});
|
|
1924
|
+
logger.info?.("Hivemind plugin registered");
|
|
1925
|
+
} catch (err) {
|
|
1926
|
+
pluginApi.logger?.error?.(`Hivemind register failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1703
1927
|
}
|
|
1704
|
-
|
|
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
|
-
}
|
|
1928
|
+
})();
|
|
1710
1929
|
}
|
|
1711
1930
|
});
|
|
1712
1931
|
export {
|