@deeplake/hivemind 0.6.47
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 +20 -0
- package/.claude-plugin/plugin.json +19 -0
- package/LICENSE +201 -0
- package/README.md +359 -0
- package/bundle/cli.js +1051 -0
- package/codex/bundle/capture.js +759 -0
- package/codex/bundle/commands/auth-login.js +862 -0
- package/codex/bundle/package.json +1 -0
- package/codex/bundle/pre-tool-use.js +2097 -0
- package/codex/bundle/session-start-setup.js +585 -0
- package/codex/bundle/session-start.js +129 -0
- package/codex/bundle/shell/deeplake-shell.js +69338 -0
- package/codex/bundle/stop.js +673 -0
- package/codex/bundle/wiki-worker.js +266 -0
- package/codex/skills/deeplake-memory/SKILL.md +65 -0
- package/cursor/bundle/capture.js +485 -0
- package/cursor/bundle/commands/auth-login.js +862 -0
- package/cursor/bundle/package.json +1 -0
- package/cursor/bundle/session-end.js +45 -0
- package/cursor/bundle/session-start.js +520 -0
- package/cursor/bundle/shell/deeplake-shell.js +69338 -0
- package/mcp/bundle/package.json +1 -0
- package/mcp/bundle/server.js +24068 -0
- package/openclaw/README.md +89 -0
- package/openclaw/dist/index.js +1714 -0
- package/openclaw/dist/package.json +1 -0
- package/openclaw/openclaw.plugin.json +56 -0
- package/openclaw/package.json +29 -0
- package/openclaw/skills/SKILL.md +61 -0
- package/package.json +69 -0
|
@@ -0,0 +1,1714 @@
|
|
|
1
|
+
// openclaw/src/setup-config.ts
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, renameSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
var HIVEMIND_TOOL_NAMES = ["hivemind_search", "hivemind_read", "hivemind_index"];
|
|
6
|
+
function getOpenclawConfigPath() {
|
|
7
|
+
return join(homedir(), ".openclaw", "openclaw.json");
|
|
8
|
+
}
|
|
9
|
+
function isAllowlistCoveringHivemind(alsoAllow) {
|
|
10
|
+
if (!Array.isArray(alsoAllow)) return false;
|
|
11
|
+
for (const entry of alsoAllow) {
|
|
12
|
+
if (typeof entry !== "string") continue;
|
|
13
|
+
const normalized = entry.trim().toLowerCase();
|
|
14
|
+
if (normalized === "hivemind") return true;
|
|
15
|
+
if (normalized === "group:plugins") return true;
|
|
16
|
+
if (HIVEMIND_TOOL_NAMES.includes(normalized)) return true;
|
|
17
|
+
}
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
function ensureHivemindAllowlisted() {
|
|
21
|
+
const configPath = getOpenclawConfigPath();
|
|
22
|
+
if (!existsSync(configPath)) {
|
|
23
|
+
return { status: "error", configPath, error: "openclaw config file not found" };
|
|
24
|
+
}
|
|
25
|
+
let parsed;
|
|
26
|
+
try {
|
|
27
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
28
|
+
parsed = JSON.parse(raw);
|
|
29
|
+
} catch (e) {
|
|
30
|
+
return { status: "error", configPath, error: `could not read/parse config: ${e instanceof Error ? e.message : String(e)}` };
|
|
31
|
+
}
|
|
32
|
+
const tools = parsed.tools ?? {};
|
|
33
|
+
const alsoAllow = Array.isArray(tools.alsoAllow) ? tools.alsoAllow : [];
|
|
34
|
+
if (isAllowlistCoveringHivemind(alsoAllow)) {
|
|
35
|
+
return { status: "already-set", configPath };
|
|
36
|
+
}
|
|
37
|
+
const updated = {
|
|
38
|
+
...parsed,
|
|
39
|
+
tools: {
|
|
40
|
+
...tools,
|
|
41
|
+
alsoAllow: [...alsoAllow, "hivemind"]
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
const backupPath = `${configPath}.bak-hivemind-${Date.now()}`;
|
|
45
|
+
const tmpPath = `${configPath}.tmp-hivemind-${process.pid}`;
|
|
46
|
+
try {
|
|
47
|
+
writeFileSync(backupPath, readFileSync(configPath, "utf-8"));
|
|
48
|
+
writeFileSync(tmpPath, JSON.stringify(updated, null, 2) + "\n");
|
|
49
|
+
renameSync(tmpPath, configPath);
|
|
50
|
+
} catch (e) {
|
|
51
|
+
return { status: "error", configPath, error: `could not write config: ${e instanceof Error ? e.message : String(e)}` };
|
|
52
|
+
}
|
|
53
|
+
return { status: "added", configPath, backupPath };
|
|
54
|
+
}
|
|
55
|
+
function toggleAutoUpdateConfig(setTo) {
|
|
56
|
+
const configPath = getOpenclawConfigPath();
|
|
57
|
+
if (!existsSync(configPath)) {
|
|
58
|
+
return { status: "error", configPath, error: "openclaw config file not found" };
|
|
59
|
+
}
|
|
60
|
+
let parsed;
|
|
61
|
+
try {
|
|
62
|
+
parsed = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
63
|
+
} catch (e) {
|
|
64
|
+
return { status: "error", configPath, error: `could not read/parse config: ${e instanceof Error ? e.message : String(e)}` };
|
|
65
|
+
}
|
|
66
|
+
const plugins = parsed.plugins ?? {};
|
|
67
|
+
const entries = plugins.entries ?? {};
|
|
68
|
+
const hivemindEntry = entries.hivemind ?? {};
|
|
69
|
+
const pluginConfig = hivemindEntry.config ?? {};
|
|
70
|
+
const current = pluginConfig.autoUpdate !== false;
|
|
71
|
+
const newValue = typeof setTo === "boolean" ? setTo : !current;
|
|
72
|
+
const updated = {
|
|
73
|
+
...parsed,
|
|
74
|
+
plugins: {
|
|
75
|
+
...plugins,
|
|
76
|
+
entries: {
|
|
77
|
+
...entries,
|
|
78
|
+
hivemind: {
|
|
79
|
+
...hivemindEntry,
|
|
80
|
+
config: { ...pluginConfig, autoUpdate: newValue }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
const backupPath = `${configPath}.bak-hivemind-${Date.now()}`;
|
|
86
|
+
const tmpPath = `${configPath}.tmp-hivemind-${process.pid}`;
|
|
87
|
+
try {
|
|
88
|
+
writeFileSync(backupPath, readFileSync(configPath, "utf-8"));
|
|
89
|
+
writeFileSync(tmpPath, JSON.stringify(updated, null, 2) + "\n");
|
|
90
|
+
renameSync(tmpPath, configPath);
|
|
91
|
+
} catch (e) {
|
|
92
|
+
return { status: "error", configPath, error: `could not write config: ${e instanceof Error ? e.message : String(e)}` };
|
|
93
|
+
}
|
|
94
|
+
return { status: "updated", configPath, newValue };
|
|
95
|
+
}
|
|
96
|
+
function detectAllowlistMissing() {
|
|
97
|
+
const configPath = getOpenclawConfigPath();
|
|
98
|
+
if (!existsSync(configPath)) return false;
|
|
99
|
+
try {
|
|
100
|
+
const parsed = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
101
|
+
const tools = parsed.tools ?? {};
|
|
102
|
+
return !isAllowlistCoveringHivemind(tools.alsoAllow);
|
|
103
|
+
} catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// src/config.ts
|
|
109
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "node:fs";
|
|
110
|
+
import { join as join2 } from "node:path";
|
|
111
|
+
import { homedir as homedir2, userInfo } from "node:os";
|
|
112
|
+
function loadConfig() {
|
|
113
|
+
const home = homedir2();
|
|
114
|
+
const credPath = join2(home, ".deeplake", "credentials.json");
|
|
115
|
+
let creds = null;
|
|
116
|
+
if (existsSync2(credPath)) {
|
|
117
|
+
try {
|
|
118
|
+
creds = JSON.parse(readFileSync2(credPath, "utf-8"));
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const token = creds?.token;
|
|
124
|
+
const orgId = creds?.orgId;
|
|
125
|
+
if (!token || !orgId) return null;
|
|
126
|
+
return {
|
|
127
|
+
token,
|
|
128
|
+
orgId,
|
|
129
|
+
orgName: creds?.orgName ?? orgId,
|
|
130
|
+
userName: creds?.userName || userInfo().username || "unknown",
|
|
131
|
+
workspaceId: creds?.workspaceId ?? "default",
|
|
132
|
+
apiUrl: creds?.apiUrl ?? "https://api.deeplake.ai",
|
|
133
|
+
tableName: "memory",
|
|
134
|
+
sessionsTableName: "sessions",
|
|
135
|
+
memoryPath: join2(home, ".deeplake", "memory")
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 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
|
+
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
|
+
async function apiGet(path, token, apiUrl, orgId) {
|
|
159
|
+
const headers = {
|
|
160
|
+
Authorization: `Bearer ${token}`,
|
|
161
|
+
"Content-Type": "application/json"
|
|
162
|
+
};
|
|
163
|
+
if (orgId) headers["X-Activeloop-Org-Id"] = orgId;
|
|
164
|
+
const resp = await fetch(`${apiUrl}${path}`, { headers });
|
|
165
|
+
if (!resp.ok) throw new Error(`API ${resp.status}: ${await resp.text().catch(() => "")}`);
|
|
166
|
+
return resp.json();
|
|
167
|
+
}
|
|
168
|
+
async function requestDeviceCode(apiUrl = DEFAULT_API_URL) {
|
|
169
|
+
const resp = await fetch(`${apiUrl}/auth/device/code`, {
|
|
170
|
+
method: "POST",
|
|
171
|
+
headers: { "Content-Type": "application/json" }
|
|
172
|
+
});
|
|
173
|
+
if (!resp.ok) throw new Error(`Device flow unavailable: HTTP ${resp.status}`);
|
|
174
|
+
return resp.json();
|
|
175
|
+
}
|
|
176
|
+
async function pollForToken(deviceCode, apiUrl = DEFAULT_API_URL) {
|
|
177
|
+
const resp = await fetch(`${apiUrl}/auth/device/token`, {
|
|
178
|
+
method: "POST",
|
|
179
|
+
headers: { "Content-Type": "application/json" },
|
|
180
|
+
body: JSON.stringify({ device_code: deviceCode })
|
|
181
|
+
});
|
|
182
|
+
if (resp.ok) return resp.json();
|
|
183
|
+
if (resp.status === 400) {
|
|
184
|
+
const err = await resp.json().catch(() => null);
|
|
185
|
+
if (err?.error === "authorization_pending" || err?.error === "slow_down") return null;
|
|
186
|
+
if (err?.error === "expired_token") throw new Error("Device code expired. Try again.");
|
|
187
|
+
if (err?.error === "access_denied") throw new Error("Authorization denied.");
|
|
188
|
+
}
|
|
189
|
+
throw new Error(`Token polling failed: HTTP ${resp.status}`);
|
|
190
|
+
}
|
|
191
|
+
async function listOrgs(token, apiUrl = DEFAULT_API_URL) {
|
|
192
|
+
const data = await apiGet("/organizations", token, apiUrl);
|
|
193
|
+
return Array.isArray(data) ? data : [];
|
|
194
|
+
}
|
|
195
|
+
async function switchOrg(orgId, orgName) {
|
|
196
|
+
const creds = loadCredentials();
|
|
197
|
+
if (!creds) throw new Error("Not logged in. Run deeplake login first.");
|
|
198
|
+
saveCredentials({ ...creds, orgId, orgName });
|
|
199
|
+
}
|
|
200
|
+
async function listWorkspaces(token, apiUrl = DEFAULT_API_URL, orgId) {
|
|
201
|
+
const raw = await apiGet("/workspaces", token, apiUrl, orgId);
|
|
202
|
+
const data = raw.data ?? raw;
|
|
203
|
+
return Array.isArray(data) ? data : [];
|
|
204
|
+
}
|
|
205
|
+
async function switchWorkspace(workspaceId) {
|
|
206
|
+
const creds = loadCredentials();
|
|
207
|
+
if (!creds) throw new Error("Not logged in. Run deeplake login first.");
|
|
208
|
+
saveCredentials({ ...creds, workspaceId });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// src/deeplake-api.ts
|
|
212
|
+
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
|
+
|
|
217
|
+
// src/utils/debug.ts
|
|
218
|
+
import { appendFileSync } from "node:fs";
|
|
219
|
+
import { join as join4 } from "node:path";
|
|
220
|
+
import { homedir as homedir4 } from "node:os";
|
|
221
|
+
var DEBUG = false;
|
|
222
|
+
var LOG = join4(homedir4(), ".deeplake", "hook-debug.log");
|
|
223
|
+
function log(tag, msg) {
|
|
224
|
+
if (!DEBUG) return;
|
|
225
|
+
appendFileSync(LOG, `${(/* @__PURE__ */ new Date()).toISOString()} [${tag}] ${msg}
|
|
226
|
+
`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// src/utils/sql.ts
|
|
230
|
+
function sqlStr(value) {
|
|
231
|
+
return value.replace(/\\/g, "\\\\").replace(/'/g, "''").replace(/\0/g, "").replace(/[\x01-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
|
|
232
|
+
}
|
|
233
|
+
function sqlLike(value) {
|
|
234
|
+
return sqlStr(value).replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// src/deeplake-api.ts
|
|
238
|
+
var log2 = (msg) => log("sdk", msg);
|
|
239
|
+
function summarizeSql(sql, maxLen = 220) {
|
|
240
|
+
const compact = sql.replace(/\s+/g, " ").trim();
|
|
241
|
+
return compact.length > maxLen ? `${compact.slice(0, maxLen)}...` : compact;
|
|
242
|
+
}
|
|
243
|
+
function traceSql(msg) {
|
|
244
|
+
const traceEnabled = false;
|
|
245
|
+
if (!traceEnabled) return;
|
|
246
|
+
process.stderr.write(`[deeplake-sql] ${msg}
|
|
247
|
+
`);
|
|
248
|
+
if (false) log2(msg);
|
|
249
|
+
}
|
|
250
|
+
var RETRYABLE_CODES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
251
|
+
var MAX_RETRIES = 3;
|
|
252
|
+
var BASE_DELAY_MS = 500;
|
|
253
|
+
var MAX_CONCURRENCY = 5;
|
|
254
|
+
var QUERY_TIMEOUT_MS = Number(1e4);
|
|
255
|
+
var INDEX_MARKER_TTL_MS = Number(6 * 60 * 6e4);
|
|
256
|
+
function sleep(ms) {
|
|
257
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
258
|
+
}
|
|
259
|
+
function isTimeoutError(error) {
|
|
260
|
+
const name = error instanceof Error ? error.name.toLowerCase() : "";
|
|
261
|
+
const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
262
|
+
return name.includes("timeout") || name === "aborterror" || message.includes("timeout") || message.includes("timed out");
|
|
263
|
+
}
|
|
264
|
+
function isDuplicateIndexError(error) {
|
|
265
|
+
const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
266
|
+
return message.includes("duplicate key value violates unique constraint") || message.includes("pg_class_relname_nsp_index") || message.includes("already exists");
|
|
267
|
+
}
|
|
268
|
+
function isSessionInsertQuery(sql) {
|
|
269
|
+
return /^\s*insert\s+into\s+"[^"]+"\s*\(\s*id\s*,\s*path\s*,\s*filename\s*,\s*message\s*,/i.test(sql);
|
|
270
|
+
}
|
|
271
|
+
function isTransientHtml403(text) {
|
|
272
|
+
const body = text.toLowerCase();
|
|
273
|
+
return body.includes("<html") || body.includes("403 forbidden") || body.includes("cloudflare") || body.includes("nginx");
|
|
274
|
+
}
|
|
275
|
+
function getIndexMarkerDir() {
|
|
276
|
+
return join5(tmpdir(), "hivemind-deeplake-indexes");
|
|
277
|
+
}
|
|
278
|
+
var Semaphore = class {
|
|
279
|
+
constructor(max) {
|
|
280
|
+
this.max = max;
|
|
281
|
+
}
|
|
282
|
+
max;
|
|
283
|
+
waiting = [];
|
|
284
|
+
active = 0;
|
|
285
|
+
async acquire() {
|
|
286
|
+
if (this.active < this.max) {
|
|
287
|
+
this.active++;
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
await new Promise((resolve) => this.waiting.push(resolve));
|
|
291
|
+
}
|
|
292
|
+
release() {
|
|
293
|
+
this.active--;
|
|
294
|
+
const next = this.waiting.shift();
|
|
295
|
+
if (next) {
|
|
296
|
+
this.active++;
|
|
297
|
+
next();
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
var DeeplakeApi = class {
|
|
302
|
+
constructor(token, apiUrl, orgId, workspaceId, tableName) {
|
|
303
|
+
this.token = token;
|
|
304
|
+
this.apiUrl = apiUrl;
|
|
305
|
+
this.orgId = orgId;
|
|
306
|
+
this.workspaceId = workspaceId;
|
|
307
|
+
this.tableName = tableName;
|
|
308
|
+
}
|
|
309
|
+
token;
|
|
310
|
+
apiUrl;
|
|
311
|
+
orgId;
|
|
312
|
+
workspaceId;
|
|
313
|
+
tableName;
|
|
314
|
+
_pendingRows = [];
|
|
315
|
+
_sem = new Semaphore(MAX_CONCURRENCY);
|
|
316
|
+
_tablesCache = null;
|
|
317
|
+
/** Execute SQL with retry on transient errors and bounded concurrency. */
|
|
318
|
+
async query(sql) {
|
|
319
|
+
const startedAt = Date.now();
|
|
320
|
+
const summary = summarizeSql(sql);
|
|
321
|
+
traceSql(`query start: ${summary}`);
|
|
322
|
+
await this._sem.acquire();
|
|
323
|
+
try {
|
|
324
|
+
const rows = await this._queryWithRetry(sql);
|
|
325
|
+
traceSql(`query ok (${Date.now() - startedAt}ms, rows=${rows.length}): ${summary}`);
|
|
326
|
+
return rows;
|
|
327
|
+
} catch (e) {
|
|
328
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
329
|
+
traceSql(`query fail (${Date.now() - startedAt}ms): ${summary} :: ${message}`);
|
|
330
|
+
throw e;
|
|
331
|
+
} finally {
|
|
332
|
+
this._sem.release();
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
async _queryWithRetry(sql) {
|
|
336
|
+
let lastError;
|
|
337
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
338
|
+
let resp;
|
|
339
|
+
try {
|
|
340
|
+
const signal = AbortSignal.timeout(QUERY_TIMEOUT_MS);
|
|
341
|
+
resp = await fetch(`${this.apiUrl}/workspaces/${this.workspaceId}/tables/query`, {
|
|
342
|
+
method: "POST",
|
|
343
|
+
headers: {
|
|
344
|
+
Authorization: `Bearer ${this.token}`,
|
|
345
|
+
"Content-Type": "application/json",
|
|
346
|
+
"X-Activeloop-Org-Id": this.orgId
|
|
347
|
+
},
|
|
348
|
+
signal,
|
|
349
|
+
body: JSON.stringify({ query: sql })
|
|
350
|
+
});
|
|
351
|
+
} catch (e) {
|
|
352
|
+
if (isTimeoutError(e)) {
|
|
353
|
+
lastError = new Error(`Query timeout after ${QUERY_TIMEOUT_MS}ms`);
|
|
354
|
+
throw lastError;
|
|
355
|
+
}
|
|
356
|
+
lastError = e instanceof Error ? e : new Error(String(e));
|
|
357
|
+
if (attempt < MAX_RETRIES) {
|
|
358
|
+
const delay = BASE_DELAY_MS * Math.pow(2, attempt) + Math.random() * 200;
|
|
359
|
+
log2(`query retry ${attempt + 1}/${MAX_RETRIES} (fetch error: ${lastError.message}) in ${delay.toFixed(0)}ms`);
|
|
360
|
+
await sleep(delay);
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
throw lastError;
|
|
364
|
+
}
|
|
365
|
+
if (resp.ok) {
|
|
366
|
+
const raw = await resp.json();
|
|
367
|
+
if (!raw?.rows || !raw?.columns) return [];
|
|
368
|
+
return raw.rows.map(
|
|
369
|
+
(row) => Object.fromEntries(raw.columns.map((col, i) => [col, row[i]]))
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
const text = await resp.text().catch(() => "");
|
|
373
|
+
const retryable403 = isSessionInsertQuery(sql) && (resp.status === 401 || resp.status === 403 && (text.length === 0 || isTransientHtml403(text)));
|
|
374
|
+
if (attempt < MAX_RETRIES && (RETRYABLE_CODES.has(resp.status) || retryable403)) {
|
|
375
|
+
const delay = BASE_DELAY_MS * Math.pow(2, attempt) + Math.random() * 200;
|
|
376
|
+
log2(`query retry ${attempt + 1}/${MAX_RETRIES} (${resp.status}) in ${delay.toFixed(0)}ms`);
|
|
377
|
+
await sleep(delay);
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
throw new Error(`Query failed: ${resp.status}: ${text.slice(0, 200)}`);
|
|
381
|
+
}
|
|
382
|
+
throw lastError ?? new Error("Query failed: max retries exceeded");
|
|
383
|
+
}
|
|
384
|
+
// ── Writes ──────────────────────────────────────────────────────────────────
|
|
385
|
+
/** Queue rows for writing. Call commit() to flush. */
|
|
386
|
+
appendRows(rows) {
|
|
387
|
+
this._pendingRows.push(...rows);
|
|
388
|
+
}
|
|
389
|
+
/** Flush pending rows via SQL. */
|
|
390
|
+
async commit() {
|
|
391
|
+
if (this._pendingRows.length === 0) return;
|
|
392
|
+
const rows = this._pendingRows;
|
|
393
|
+
this._pendingRows = [];
|
|
394
|
+
const CONCURRENCY = 10;
|
|
395
|
+
for (let i = 0; i < rows.length; i += CONCURRENCY) {
|
|
396
|
+
const chunk = rows.slice(i, i + CONCURRENCY);
|
|
397
|
+
await Promise.allSettled(chunk.map((r) => this.upsertRowSql(r)));
|
|
398
|
+
}
|
|
399
|
+
log2(`commit: ${rows.length} rows`);
|
|
400
|
+
}
|
|
401
|
+
async upsertRowSql(row) {
|
|
402
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
403
|
+
const cd = row.creationDate ?? ts;
|
|
404
|
+
const lud = row.lastUpdateDate ?? ts;
|
|
405
|
+
const exists = await this.query(
|
|
406
|
+
`SELECT path FROM "${this.tableName}" WHERE path = '${sqlStr(row.path)}' LIMIT 1`
|
|
407
|
+
);
|
|
408
|
+
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}'`;
|
|
410
|
+
if (row.project !== void 0) setClauses += `, project = '${sqlStr(row.project)}'`;
|
|
411
|
+
if (row.description !== void 0) setClauses += `, description = '${sqlStr(row.description)}'`;
|
|
412
|
+
await this.query(
|
|
413
|
+
`UPDATE "${this.tableName}" SET ${setClauses} WHERE path = '${sqlStr(row.path)}'`
|
|
414
|
+
);
|
|
415
|
+
} else {
|
|
416
|
+
const id = randomUUID();
|
|
417
|
+
let cols = "id, path, filename, summary, mime_type, size_bytes, creation_date, last_update_date";
|
|
418
|
+
let vals = `'${id}', '${sqlStr(row.path)}', '${sqlStr(row.filename)}', E'${sqlStr(row.contentText)}', '${sqlStr(row.mimeType)}', ${row.sizeBytes}, '${cd}', '${lud}'`;
|
|
419
|
+
if (row.project !== void 0) {
|
|
420
|
+
cols += ", project";
|
|
421
|
+
vals += `, '${sqlStr(row.project)}'`;
|
|
422
|
+
}
|
|
423
|
+
if (row.description !== void 0) {
|
|
424
|
+
cols += ", description";
|
|
425
|
+
vals += `, '${sqlStr(row.description)}'`;
|
|
426
|
+
}
|
|
427
|
+
await this.query(
|
|
428
|
+
`INSERT INTO "${this.tableName}" (${cols}) VALUES (${vals})`
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
/** Update specific columns on a row by path. */
|
|
433
|
+
async updateColumns(path, columns) {
|
|
434
|
+
const setClauses = Object.entries(columns).map(([col, val]) => typeof val === "number" ? `${col} = ${val}` : `${col} = '${sqlStr(String(val))}'`).join(", ");
|
|
435
|
+
await this.query(
|
|
436
|
+
`UPDATE "${this.tableName}" SET ${setClauses} WHERE path = '${sqlStr(path)}'`
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
// ── Convenience ─────────────────────────────────────────────────────────────
|
|
440
|
+
/** Create a BM25 search index on a column. */
|
|
441
|
+
async createIndex(column) {
|
|
442
|
+
await this.query(`CREATE INDEX IF NOT EXISTS idx_${sqlStr(column)}_bm25 ON "${this.tableName}" USING deeplake_index ("${column}")`);
|
|
443
|
+
}
|
|
444
|
+
buildLookupIndexName(table, suffix) {
|
|
445
|
+
return `idx_${table}_${suffix}`.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
446
|
+
}
|
|
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
|
+
async ensureLookupIndex(table, suffix, columnsSql) {
|
|
477
|
+
if (this.hasFreshLookupIndexMarker(table, suffix)) return;
|
|
478
|
+
const indexName = this.buildLookupIndexName(table, suffix);
|
|
479
|
+
try {
|
|
480
|
+
await this.query(`CREATE INDEX IF NOT EXISTS "${indexName}" ON "${table}" ${columnsSql}`);
|
|
481
|
+
this.markLookupIndexReady(table, suffix);
|
|
482
|
+
} catch (e) {
|
|
483
|
+
if (isDuplicateIndexError(e)) {
|
|
484
|
+
this.markLookupIndexReady(table, suffix);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
log2(`index "${indexName}" skipped: ${e.message}`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
/** List all tables in the workspace (with retry). */
|
|
491
|
+
async listTables(forceRefresh = false) {
|
|
492
|
+
if (!forceRefresh && this._tablesCache) return [...this._tablesCache];
|
|
493
|
+
const { tables, cacheable } = await this._fetchTables();
|
|
494
|
+
if (cacheable) this._tablesCache = [...tables];
|
|
495
|
+
return tables;
|
|
496
|
+
}
|
|
497
|
+
async _fetchTables() {
|
|
498
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
499
|
+
try {
|
|
500
|
+
const resp = await fetch(`${this.apiUrl}/workspaces/${this.workspaceId}/tables`, {
|
|
501
|
+
headers: {
|
|
502
|
+
Authorization: `Bearer ${this.token}`,
|
|
503
|
+
"X-Activeloop-Org-Id": this.orgId
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
if (resp.ok) {
|
|
507
|
+
const data = await resp.json();
|
|
508
|
+
return {
|
|
509
|
+
tables: (data.tables ?? []).map((t) => t.table_name),
|
|
510
|
+
cacheable: true
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
if (attempt < MAX_RETRIES && RETRYABLE_CODES.has(resp.status)) {
|
|
514
|
+
await sleep(BASE_DELAY_MS * Math.pow(2, attempt) + Math.random() * 200);
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
return { tables: [], cacheable: false };
|
|
518
|
+
} catch {
|
|
519
|
+
if (attempt < MAX_RETRIES) {
|
|
520
|
+
await sleep(BASE_DELAY_MS * Math.pow(2, attempt));
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
return { tables: [], cacheable: false };
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return { tables: [], cacheable: false };
|
|
527
|
+
}
|
|
528
|
+
/** Create the memory table if it doesn't already exist. Migrate columns on existing tables. */
|
|
529
|
+
async ensureTable(name) {
|
|
530
|
+
const tbl = name ?? this.tableName;
|
|
531
|
+
const tables = await this.listTables();
|
|
532
|
+
if (!tables.includes(tbl)) {
|
|
533
|
+
log2(`table "${tbl}" not found, creating`);
|
|
534
|
+
await this.query(
|
|
535
|
+
`CREATE TABLE IF NOT EXISTS "${tbl}" (id TEXT NOT NULL DEFAULT '', path TEXT NOT NULL DEFAULT '', filename TEXT NOT NULL DEFAULT '', summary TEXT NOT NULL DEFAULT '', author TEXT NOT NULL DEFAULT '', mime_type TEXT NOT NULL DEFAULT 'text/plain', size_bytes BIGINT NOT NULL DEFAULT 0, project TEXT NOT NULL DEFAULT '', description TEXT NOT NULL DEFAULT '', agent TEXT NOT NULL DEFAULT '', creation_date TEXT NOT NULL DEFAULT '', last_update_date TEXT NOT NULL DEFAULT '') USING deeplake`
|
|
536
|
+
);
|
|
537
|
+
log2(`table "${tbl}" created`);
|
|
538
|
+
if (!tables.includes(tbl)) this._tablesCache = [...tables, tbl];
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
/** Create the sessions table (uses JSONB for message since every row is a JSON event). */
|
|
542
|
+
async ensureSessionsTable(name) {
|
|
543
|
+
const tables = await this.listTables();
|
|
544
|
+
if (!tables.includes(name)) {
|
|
545
|
+
log2(`table "${name}" not found, creating`);
|
|
546
|
+
await this.query(
|
|
547
|
+
`CREATE TABLE IF NOT EXISTS "${name}" (id TEXT NOT NULL DEFAULT '', path TEXT NOT NULL DEFAULT '', filename TEXT NOT NULL DEFAULT '', message JSONB, author TEXT NOT NULL DEFAULT '', mime_type TEXT NOT NULL DEFAULT 'application/json', size_bytes BIGINT NOT NULL DEFAULT 0, project TEXT NOT NULL DEFAULT '', description TEXT NOT NULL DEFAULT '', agent TEXT NOT NULL DEFAULT '', creation_date TEXT NOT NULL DEFAULT '', last_update_date TEXT NOT NULL DEFAULT '') USING deeplake`
|
|
548
|
+
);
|
|
549
|
+
log2(`table "${name}" created`);
|
|
550
|
+
if (!tables.includes(name)) this._tablesCache = [...tables, name];
|
|
551
|
+
}
|
|
552
|
+
await this.ensureLookupIndex(name, "path_creation_date", `("path", "creation_date")`);
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
// src/shell/grep-core.ts
|
|
557
|
+
var TOOL_INPUT_FIELDS = [
|
|
558
|
+
"command",
|
|
559
|
+
"file_path",
|
|
560
|
+
"path",
|
|
561
|
+
"pattern",
|
|
562
|
+
"prompt",
|
|
563
|
+
"subagent_type",
|
|
564
|
+
"query",
|
|
565
|
+
"url",
|
|
566
|
+
"notebook_path",
|
|
567
|
+
"old_string",
|
|
568
|
+
"new_string",
|
|
569
|
+
"content",
|
|
570
|
+
"skill",
|
|
571
|
+
"args",
|
|
572
|
+
"taskId",
|
|
573
|
+
"status",
|
|
574
|
+
"subject",
|
|
575
|
+
"description",
|
|
576
|
+
"to",
|
|
577
|
+
"message",
|
|
578
|
+
"summary",
|
|
579
|
+
"max_results"
|
|
580
|
+
];
|
|
581
|
+
var TOOL_RESPONSE_DROP = /* @__PURE__ */ new Set([
|
|
582
|
+
// Note: `stderr` is intentionally NOT in this set. The `stdout` high-signal
|
|
583
|
+
// branch below already de-dupes it for the common case (appends as suffix
|
|
584
|
+
// when non-empty). If a tool response has ONLY `stderr` and no `stdout`
|
|
585
|
+
// (hard-failure on some tools), the generic cleanup preserves it so the
|
|
586
|
+
// error message reaches Claude instead of collapsing to `[ok]`.
|
|
587
|
+
"interrupted",
|
|
588
|
+
"isImage",
|
|
589
|
+
"noOutputExpected",
|
|
590
|
+
"type",
|
|
591
|
+
"structuredPatch",
|
|
592
|
+
"userModified",
|
|
593
|
+
"originalFile",
|
|
594
|
+
"replaceAll",
|
|
595
|
+
"totalDurationMs",
|
|
596
|
+
"totalTokens",
|
|
597
|
+
"totalToolUseCount",
|
|
598
|
+
"usage",
|
|
599
|
+
"toolStats",
|
|
600
|
+
"durationMs",
|
|
601
|
+
"durationSeconds",
|
|
602
|
+
"bytes",
|
|
603
|
+
"code",
|
|
604
|
+
"codeText",
|
|
605
|
+
"agentId",
|
|
606
|
+
"agentType",
|
|
607
|
+
"verificationNudgeNeeded",
|
|
608
|
+
"numLines",
|
|
609
|
+
"numFiles",
|
|
610
|
+
"truncated",
|
|
611
|
+
"statusChange",
|
|
612
|
+
"updatedFields",
|
|
613
|
+
"isAgent",
|
|
614
|
+
"success"
|
|
615
|
+
]);
|
|
616
|
+
function maybeParseJson(v) {
|
|
617
|
+
if (typeof v !== "string") return v;
|
|
618
|
+
const s = v.trim();
|
|
619
|
+
if (s[0] !== "{" && s[0] !== "[") return v;
|
|
620
|
+
try {
|
|
621
|
+
return JSON.parse(s);
|
|
622
|
+
} catch {
|
|
623
|
+
return v;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
function snakeCase(k) {
|
|
627
|
+
return k.replace(/([A-Z])/g, "_$1").toLowerCase();
|
|
628
|
+
}
|
|
629
|
+
function camelCase(k) {
|
|
630
|
+
return k.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
631
|
+
}
|
|
632
|
+
function formatToolInput(raw) {
|
|
633
|
+
const p = maybeParseJson(raw);
|
|
634
|
+
if (typeof p !== "object" || p === null) return String(p ?? "");
|
|
635
|
+
const parts = [];
|
|
636
|
+
for (const k of TOOL_INPUT_FIELDS) {
|
|
637
|
+
if (p[k] === void 0) continue;
|
|
638
|
+
const v = p[k];
|
|
639
|
+
parts.push(`${k}: ${typeof v === "string" ? v : JSON.stringify(v)}`);
|
|
640
|
+
}
|
|
641
|
+
for (const k of ["glob", "output_mode", "limit", "offset"]) {
|
|
642
|
+
if (p[k] !== void 0) parts.push(`${k}: ${p[k]}`);
|
|
643
|
+
}
|
|
644
|
+
return parts.length ? parts.join("\n") : JSON.stringify(p);
|
|
645
|
+
}
|
|
646
|
+
function formatToolResponse(raw, inp, toolName) {
|
|
647
|
+
const r = maybeParseJson(raw);
|
|
648
|
+
if (typeof r !== "object" || r === null) return String(r ?? "");
|
|
649
|
+
if (toolName === "Edit" || toolName === "Write" || toolName === "MultiEdit") {
|
|
650
|
+
return r.filePath ? `[wrote ${r.filePath}]` : "[ok]";
|
|
651
|
+
}
|
|
652
|
+
if (typeof r.stdout === "string") {
|
|
653
|
+
const stderr = r.stderr;
|
|
654
|
+
return r.stdout + (stderr ? `
|
|
655
|
+
stderr: ${stderr}` : "");
|
|
656
|
+
}
|
|
657
|
+
if (typeof r.content === "string") return r.content;
|
|
658
|
+
if (r.file && typeof r.file === "object") {
|
|
659
|
+
const f = r.file;
|
|
660
|
+
if (typeof f.content === "string") return `[${f.filePath ?? ""}]
|
|
661
|
+
${f.content}`;
|
|
662
|
+
if (typeof f.base64 === "string") return `[binary ${f.filePath ?? ""}: ${f.base64.length} base64 chars]`;
|
|
663
|
+
}
|
|
664
|
+
if (Array.isArray(r.filenames)) return r.filenames.join("\n");
|
|
665
|
+
if (Array.isArray(r.matches)) {
|
|
666
|
+
return r.matches.map((m) => typeof m === "string" ? m : JSON.stringify(m)).join("\n");
|
|
667
|
+
}
|
|
668
|
+
if (Array.isArray(r.results)) {
|
|
669
|
+
return r.results.map((x) => typeof x === "string" ? x : x?.title ?? x?.url ?? JSON.stringify(x)).join("\n");
|
|
670
|
+
}
|
|
671
|
+
const inpObj = maybeParseJson(inp);
|
|
672
|
+
const kept = {};
|
|
673
|
+
for (const [k, v] of Object.entries(r)) {
|
|
674
|
+
if (TOOL_RESPONSE_DROP.has(k)) continue;
|
|
675
|
+
if (v === "" || v === false || v == null) continue;
|
|
676
|
+
if (typeof inpObj === "object" && inpObj) {
|
|
677
|
+
const inObj = inpObj;
|
|
678
|
+
if (k in inObj && JSON.stringify(inObj[k]) === JSON.stringify(v)) continue;
|
|
679
|
+
const snake = snakeCase(k);
|
|
680
|
+
if (snake in inObj && JSON.stringify(inObj[snake]) === JSON.stringify(v)) continue;
|
|
681
|
+
const camel = camelCase(k);
|
|
682
|
+
if (camel in inObj && JSON.stringify(inObj[camel]) === JSON.stringify(v)) continue;
|
|
683
|
+
}
|
|
684
|
+
kept[k] = v;
|
|
685
|
+
}
|
|
686
|
+
return Object.keys(kept).length ? JSON.stringify(kept) : "[ok]";
|
|
687
|
+
}
|
|
688
|
+
function formatToolCall(obj) {
|
|
689
|
+
return `[tool:${obj?.tool_name ?? "?"}]
|
|
690
|
+
input: ${formatToolInput(obj?.tool_input)}
|
|
691
|
+
response: ${formatToolResponse(obj?.tool_response, obj?.tool_input, obj?.tool_name)}`;
|
|
692
|
+
}
|
|
693
|
+
function normalizeContent(path, raw) {
|
|
694
|
+
if (!path.includes("/sessions/")) return raw;
|
|
695
|
+
if (!raw || raw[0] !== "{") return raw;
|
|
696
|
+
let obj;
|
|
697
|
+
try {
|
|
698
|
+
obj = JSON.parse(raw);
|
|
699
|
+
} catch {
|
|
700
|
+
return raw;
|
|
701
|
+
}
|
|
702
|
+
if (Array.isArray(obj.turns)) {
|
|
703
|
+
const header = [];
|
|
704
|
+
if (obj.date_time) header.push(`date: ${obj.date_time}`);
|
|
705
|
+
if (obj.speakers) {
|
|
706
|
+
const s = obj.speakers;
|
|
707
|
+
const names = [s.speaker_a, s.speaker_b].filter(Boolean).join(", ");
|
|
708
|
+
if (names) header.push(`speakers: ${names}`);
|
|
709
|
+
}
|
|
710
|
+
const lines = obj.turns.map((t) => {
|
|
711
|
+
const sp = String(t?.speaker ?? t?.name ?? "?").trim();
|
|
712
|
+
const tx = String(t?.text ?? t?.content ?? "").replace(/\s+/g, " ").trim();
|
|
713
|
+
const tag = t?.dia_id ? `[${t.dia_id}] ` : "";
|
|
714
|
+
return `${tag}${sp}: ${tx}`;
|
|
715
|
+
});
|
|
716
|
+
const out2 = [...header, ...lines].join("\n");
|
|
717
|
+
return out2.trim() ? out2 : raw;
|
|
718
|
+
}
|
|
719
|
+
const stripRecalled = (t) => {
|
|
720
|
+
const i = t.indexOf("<recalled-memories>");
|
|
721
|
+
if (i === -1) return t;
|
|
722
|
+
const j = t.lastIndexOf("</recalled-memories>");
|
|
723
|
+
if (j === -1 || j < i) return t;
|
|
724
|
+
const head = t.slice(0, i);
|
|
725
|
+
const tail = t.slice(j + "</recalled-memories>".length);
|
|
726
|
+
return (head + tail).replace(/^\s+/, "").replace(/\n{3,}/g, "\n\n");
|
|
727
|
+
};
|
|
728
|
+
let out = null;
|
|
729
|
+
if (obj.type === "user_message") {
|
|
730
|
+
out = `[user] ${stripRecalled(String(obj.content ?? ""))}`;
|
|
731
|
+
} else if (obj.type === "assistant_message") {
|
|
732
|
+
const agent = obj.agent_type ? ` (agent=${obj.agent_type})` : "";
|
|
733
|
+
out = `[assistant${agent}] ${stripRecalled(String(obj.content ?? ""))}`;
|
|
734
|
+
} else if (obj.type === "tool_call") {
|
|
735
|
+
out = formatToolCall(obj);
|
|
736
|
+
}
|
|
737
|
+
if (out === null) return raw;
|
|
738
|
+
const trimmed = out.trim();
|
|
739
|
+
if (!trimmed || trimmed === "[user]" || trimmed === "[assistant]" || /^\[tool:[^\]]*\]\s+input:\s+\{\}\s+response:\s+\{\}$/.test(trimmed)) return raw;
|
|
740
|
+
return out;
|
|
741
|
+
}
|
|
742
|
+
function buildPathCondition(targetPath) {
|
|
743
|
+
if (!targetPath || targetPath === "/") return "";
|
|
744
|
+
const clean = targetPath.replace(/\/+$/, "");
|
|
745
|
+
if (/[*?]/.test(clean)) {
|
|
746
|
+
const likePattern = sqlLike(clean).replace(/\*/g, "%").replace(/\?/g, "_");
|
|
747
|
+
return `path LIKE '${likePattern}' ESCAPE '\\'`;
|
|
748
|
+
}
|
|
749
|
+
const base = clean.split("/").pop() ?? "";
|
|
750
|
+
if (base.includes(".")) {
|
|
751
|
+
return `path = '${sqlStr(clean)}'`;
|
|
752
|
+
}
|
|
753
|
+
return `(path = '${sqlStr(clean)}' OR path LIKE '${sqlLike(clean)}/%' ESCAPE '\\')`;
|
|
754
|
+
}
|
|
755
|
+
async function searchDeeplakeTables(api2, memoryTable2, sessionsTable2, opts) {
|
|
756
|
+
const { pathFilter, contentScanOnly, likeOp, escapedPattern, prefilterPattern, prefilterPatterns, multiWordPatterns } = opts;
|
|
757
|
+
const limit = opts.limit ?? 100;
|
|
758
|
+
const filterPatterns = contentScanOnly ? prefilterPatterns && prefilterPatterns.length > 0 ? prefilterPatterns : prefilterPattern ? [prefilterPattern] : [] : multiWordPatterns && multiWordPatterns.length > 1 ? multiWordPatterns : [escapedPattern];
|
|
759
|
+
const memFilter = buildContentFilter("summary::text", likeOp, filterPatterns);
|
|
760
|
+
const sessFilter = buildContentFilter("message::text", likeOp, filterPatterns);
|
|
761
|
+
const memQuery = `SELECT path, summary::text AS content, 0 AS source_order, '' AS creation_date FROM "${memoryTable2}" WHERE 1=1${pathFilter}${memFilter} LIMIT ${limit}`;
|
|
762
|
+
const sessQuery = `SELECT path, message::text AS content, 1 AS source_order, COALESCE(creation_date::text, '') AS creation_date FROM "${sessionsTable2}" WHERE 1=1${pathFilter}${sessFilter} LIMIT ${limit}`;
|
|
763
|
+
const rows = await api2.query(
|
|
764
|
+
`SELECT path, content, source_order, creation_date FROM ((${memQuery}) UNION ALL (${sessQuery})) AS combined ORDER BY path, source_order, creation_date`
|
|
765
|
+
);
|
|
766
|
+
return rows.map((row) => ({
|
|
767
|
+
path: String(row["path"]),
|
|
768
|
+
content: String(row["content"] ?? "")
|
|
769
|
+
}));
|
|
770
|
+
}
|
|
771
|
+
function buildPathFilter(targetPath) {
|
|
772
|
+
const condition = buildPathCondition(targetPath);
|
|
773
|
+
return condition ? ` AND ${condition}` : "";
|
|
774
|
+
}
|
|
775
|
+
function extractRegexLiteralPrefilter(pattern) {
|
|
776
|
+
if (!pattern) return null;
|
|
777
|
+
const parts = [];
|
|
778
|
+
let current = "";
|
|
779
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
780
|
+
const ch = pattern[i];
|
|
781
|
+
if (ch === "\\") {
|
|
782
|
+
const next = pattern[i + 1];
|
|
783
|
+
if (!next) return null;
|
|
784
|
+
if (/[dDsSwWbBAZzGkKpP]/.test(next)) return null;
|
|
785
|
+
current += next;
|
|
786
|
+
i++;
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
if (ch === ".") {
|
|
790
|
+
if (pattern[i + 1] === "*") {
|
|
791
|
+
if (current) parts.push(current);
|
|
792
|
+
current = "";
|
|
793
|
+
i++;
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
return null;
|
|
797
|
+
}
|
|
798
|
+
if ("|()[]{}+?^$".includes(ch) || ch === "*") return null;
|
|
799
|
+
current += ch;
|
|
800
|
+
}
|
|
801
|
+
if (current) parts.push(current);
|
|
802
|
+
const literal = parts.reduce((best, part) => part.length > best.length ? part : best, "");
|
|
803
|
+
return literal.length >= 2 ? literal : null;
|
|
804
|
+
}
|
|
805
|
+
function extractRegexAlternationPrefilters(pattern) {
|
|
806
|
+
if (!pattern.includes("|")) return null;
|
|
807
|
+
const parts = [];
|
|
808
|
+
let current = "";
|
|
809
|
+
let escaped = false;
|
|
810
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
811
|
+
const ch = pattern[i];
|
|
812
|
+
if (escaped) {
|
|
813
|
+
current += `\\${ch}`;
|
|
814
|
+
escaped = false;
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
if (ch === "\\") {
|
|
818
|
+
escaped = true;
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
if (ch === "|") {
|
|
822
|
+
if (!current) return null;
|
|
823
|
+
parts.push(current);
|
|
824
|
+
current = "";
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
if ("()[]{}^$".includes(ch)) return null;
|
|
828
|
+
current += ch;
|
|
829
|
+
}
|
|
830
|
+
if (escaped || !current) return null;
|
|
831
|
+
parts.push(current);
|
|
832
|
+
const literals = [...new Set(
|
|
833
|
+
parts.map((part) => extractRegexLiteralPrefilter(part)).filter((part) => typeof part === "string" && part.length >= 2)
|
|
834
|
+
)];
|
|
835
|
+
return literals.length > 0 ? literals : null;
|
|
836
|
+
}
|
|
837
|
+
function buildGrepSearchOptions(params, targetPath) {
|
|
838
|
+
const hasRegexMeta = !params.fixedString && /[.*+?^${}()|[\]\\]/.test(params.pattern);
|
|
839
|
+
const literalPrefilter = hasRegexMeta ? extractRegexLiteralPrefilter(params.pattern) : null;
|
|
840
|
+
const alternationPrefilters = hasRegexMeta ? extractRegexAlternationPrefilters(params.pattern) : null;
|
|
841
|
+
const multiWordPatterns = !hasRegexMeta ? params.pattern.split(/\s+/).filter((w) => w.length > 2).slice(0, 4) : [];
|
|
842
|
+
return {
|
|
843
|
+
pathFilter: buildPathFilter(targetPath),
|
|
844
|
+
contentScanOnly: hasRegexMeta,
|
|
845
|
+
likeOp: params.ignoreCase ? "ILIKE" : "LIKE",
|
|
846
|
+
escapedPattern: sqlLike(params.pattern),
|
|
847
|
+
prefilterPattern: literalPrefilter ? sqlLike(literalPrefilter) : void 0,
|
|
848
|
+
prefilterPatterns: alternationPrefilters?.map((literal) => sqlLike(literal)),
|
|
849
|
+
multiWordPatterns: multiWordPatterns.length > 1 ? multiWordPatterns.map((w) => sqlLike(w)) : void 0
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
function buildContentFilter(column, likeOp, patterns) {
|
|
853
|
+
if (patterns.length === 0) return "";
|
|
854
|
+
if (patterns.length === 1) return ` AND ${column} ${likeOp} '%${patterns[0]}%'`;
|
|
855
|
+
return ` AND (${patterns.map((pattern) => `${column} ${likeOp} '%${pattern}%'`).join(" OR ")})`;
|
|
856
|
+
}
|
|
857
|
+
function compileGrepRegex(params) {
|
|
858
|
+
let reStr = params.fixedString ? params.pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") : params.pattern;
|
|
859
|
+
if (params.wordMatch) reStr = `\\b${reStr}\\b`;
|
|
860
|
+
try {
|
|
861
|
+
return new RegExp(reStr, params.ignoreCase ? "i" : "");
|
|
862
|
+
} catch {
|
|
863
|
+
return new RegExp(
|
|
864
|
+
params.pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"),
|
|
865
|
+
params.ignoreCase ? "i" : ""
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// src/hooks/virtual-table-query.ts
|
|
871
|
+
function normalizeSessionPart(path, content) {
|
|
872
|
+
return normalizeContent(path, content);
|
|
873
|
+
}
|
|
874
|
+
function buildVirtualIndexContent(summaryRows, sessionRows = []) {
|
|
875
|
+
const total = summaryRows.length + sessionRows.length;
|
|
876
|
+
const lines = [
|
|
877
|
+
"# Memory Index",
|
|
878
|
+
"",
|
|
879
|
+
`${total} entries (${summaryRows.length} summaries, ${sessionRows.length} sessions):`,
|
|
880
|
+
""
|
|
881
|
+
];
|
|
882
|
+
if (summaryRows.length > 0) {
|
|
883
|
+
lines.push("## Summaries", "");
|
|
884
|
+
for (const row of summaryRows) {
|
|
885
|
+
const path = row["path"];
|
|
886
|
+
const project = row["project"] || "";
|
|
887
|
+
const description = (row["description"] || "").slice(0, 120);
|
|
888
|
+
const date = (row["creation_date"] || "").slice(0, 10);
|
|
889
|
+
lines.push(`- [${path}](${path}) ${date} ${project ? `[${project}]` : ""} ${description}`);
|
|
890
|
+
}
|
|
891
|
+
lines.push("");
|
|
892
|
+
}
|
|
893
|
+
if (sessionRows.length > 0) {
|
|
894
|
+
lines.push("## Sessions", "");
|
|
895
|
+
for (const row of sessionRows) {
|
|
896
|
+
const path = row["path"];
|
|
897
|
+
const description = (row["description"] || "").slice(0, 120);
|
|
898
|
+
lines.push(`- [${path}](${path}) ${description}`);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
return lines.join("\n");
|
|
902
|
+
}
|
|
903
|
+
function buildUnionQuery(memoryQuery, sessionsQuery) {
|
|
904
|
+
return `SELECT path, content, size_bytes, creation_date, source_order FROM ((${memoryQuery}) UNION ALL (${sessionsQuery})) AS combined ORDER BY path, source_order, creation_date`;
|
|
905
|
+
}
|
|
906
|
+
function buildInList(paths) {
|
|
907
|
+
return paths.map((path) => `'${sqlStr(path)}'`).join(", ");
|
|
908
|
+
}
|
|
909
|
+
async function queryUnionRows(api2, memoryQuery, sessionsQuery) {
|
|
910
|
+
const unionQuery = buildUnionQuery(memoryQuery, sessionsQuery);
|
|
911
|
+
try {
|
|
912
|
+
return await api2.query(unionQuery);
|
|
913
|
+
} catch {
|
|
914
|
+
const [memoryRows, sessionRows] = await Promise.all([
|
|
915
|
+
api2.query(memoryQuery).catch(() => []),
|
|
916
|
+
api2.query(sessionsQuery).catch(() => [])
|
|
917
|
+
]);
|
|
918
|
+
return [...memoryRows, ...sessionRows];
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
async function readVirtualPathContents(api2, memoryTable2, sessionsTable2, virtualPaths) {
|
|
922
|
+
const uniquePaths = [...new Set(virtualPaths)];
|
|
923
|
+
const result = new Map(uniquePaths.map((path) => [path, null]));
|
|
924
|
+
if (uniquePaths.length === 0) return result;
|
|
925
|
+
const inList = buildInList(uniquePaths);
|
|
926
|
+
const rows = await queryUnionRows(
|
|
927
|
+
api2,
|
|
928
|
+
`SELECT path, summary::text AS content, NULL::bigint AS size_bytes, '' AS creation_date, 0 AS source_order FROM "${memoryTable2}" WHERE path IN (${inList})`,
|
|
929
|
+
`SELECT path, message::text AS content, NULL::bigint AS size_bytes, COALESCE(creation_date::text, '') AS creation_date, 1 AS source_order FROM "${sessionsTable2}" WHERE path IN (${inList})`
|
|
930
|
+
);
|
|
931
|
+
const memoryHits = /* @__PURE__ */ new Map();
|
|
932
|
+
const sessionHits = /* @__PURE__ */ new Map();
|
|
933
|
+
for (const row of rows) {
|
|
934
|
+
const path = row["path"];
|
|
935
|
+
const content = row["content"];
|
|
936
|
+
const sourceOrder = Number(row["source_order"] ?? 0);
|
|
937
|
+
if (typeof path !== "string" || typeof content !== "string") continue;
|
|
938
|
+
if (sourceOrder === 0) {
|
|
939
|
+
memoryHits.set(path, content);
|
|
940
|
+
} else {
|
|
941
|
+
const current = sessionHits.get(path) ?? [];
|
|
942
|
+
current.push(normalizeSessionPart(path, content));
|
|
943
|
+
sessionHits.set(path, current);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
for (const path of uniquePaths) {
|
|
947
|
+
if (memoryHits.has(path)) {
|
|
948
|
+
result.set(path, memoryHits.get(path) ?? null);
|
|
949
|
+
continue;
|
|
950
|
+
}
|
|
951
|
+
const sessionParts = sessionHits.get(path) ?? [];
|
|
952
|
+
if (sessionParts.length > 0) {
|
|
953
|
+
result.set(path, sessionParts.join("\n"));
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
if (result.get("/index.md") === null && uniquePaths.includes("/index.md")) {
|
|
957
|
+
const [summaryRows, sessionRows] = await Promise.all([
|
|
958
|
+
api2.query(
|
|
959
|
+
`SELECT path, project, description, creation_date FROM "${memoryTable2}" WHERE path LIKE '/summaries/%' ORDER BY creation_date DESC`
|
|
960
|
+
).catch(() => []),
|
|
961
|
+
api2.query(
|
|
962
|
+
`SELECT path, description FROM "${sessionsTable2}" WHERE path LIKE '/sessions/%' ORDER BY path`
|
|
963
|
+
).catch(() => [])
|
|
964
|
+
]);
|
|
965
|
+
result.set("/index.md", buildVirtualIndexContent(summaryRows, sessionRows));
|
|
966
|
+
}
|
|
967
|
+
return result;
|
|
968
|
+
}
|
|
969
|
+
async function readVirtualPathContent(api2, memoryTable2, sessionsTable2, virtualPath) {
|
|
970
|
+
return (await readVirtualPathContents(api2, memoryTable2, sessionsTable2, [virtualPath])).get(virtualPath) ?? null;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// openclaw/src/index.ts
|
|
974
|
+
function definePluginEntry(entry) {
|
|
975
|
+
return entry;
|
|
976
|
+
}
|
|
977
|
+
var DEFAULT_API_URL2 = "https://api.deeplake.ai";
|
|
978
|
+
var VERSION_URL = "https://clawhub.ai/api/v1/packages/hivemind";
|
|
979
|
+
function extractLatestVersion(body) {
|
|
980
|
+
if (typeof body !== "object" || body === null) return null;
|
|
981
|
+
const pkg = body.package;
|
|
982
|
+
if (typeof pkg !== "object" || pkg === null) return null;
|
|
983
|
+
const v = pkg.latestVersion;
|
|
984
|
+
return typeof v === "string" && v.length > 0 ? v : null;
|
|
985
|
+
}
|
|
986
|
+
function getInstalledVersion() {
|
|
987
|
+
return "0.6.47".length > 0 ? "0.6.47" : null;
|
|
988
|
+
}
|
|
989
|
+
function isNewer(latest, current) {
|
|
990
|
+
const parse = (v) => v.replace(/-.*$/, "").split(".").map(Number);
|
|
991
|
+
const [la, lb, lc] = parse(latest);
|
|
992
|
+
const [ca, cb, cc] = parse(current);
|
|
993
|
+
return la > ca || la === ca && lb > cb || la === ca && lb === cb && lc > cc;
|
|
994
|
+
}
|
|
995
|
+
async function checkForUpdate(logger) {
|
|
996
|
+
try {
|
|
997
|
+
const current = getInstalledVersion();
|
|
998
|
+
if (!current) return;
|
|
999
|
+
const res = await fetch(VERSION_URL, { signal: AbortSignal.timeout(3e3) });
|
|
1000
|
+
if (!res.ok) return;
|
|
1001
|
+
const latest = extractLatestVersion(await res.json());
|
|
1002
|
+
if (latest && isNewer(latest, current)) {
|
|
1003
|
+
logger.info?.(`\u2B06\uFE0F Hivemind update available: ${current} \u2192 ${latest}. Run: openclaw plugins update hivemind`);
|
|
1004
|
+
}
|
|
1005
|
+
} catch {
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
var authPending = false;
|
|
1009
|
+
var authUrl = null;
|
|
1010
|
+
var pendingUpdate = null;
|
|
1011
|
+
var justAuthenticated = false;
|
|
1012
|
+
async function requestAuth() {
|
|
1013
|
+
if (authPending) return authUrl ?? "";
|
|
1014
|
+
authPending = true;
|
|
1015
|
+
try {
|
|
1016
|
+
const code = await requestDeviceCode();
|
|
1017
|
+
authUrl = code.verification_uri_complete;
|
|
1018
|
+
const pollMs = Math.max(code.interval || 5, 5) * 1e3;
|
|
1019
|
+
const deadline = Date.now() + code.expires_in * 1e3;
|
|
1020
|
+
(async () => {
|
|
1021
|
+
while (Date.now() < deadline && authPending) {
|
|
1022
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
1023
|
+
try {
|
|
1024
|
+
const result = await pollForToken(code.device_code);
|
|
1025
|
+
if (result) {
|
|
1026
|
+
const token = result.access_token;
|
|
1027
|
+
let userName;
|
|
1028
|
+
try {
|
|
1029
|
+
const meResp = await fetch(`${DEFAULT_API_URL2}/me`, {
|
|
1030
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
1031
|
+
});
|
|
1032
|
+
if (meResp.ok) {
|
|
1033
|
+
const me = await meResp.json();
|
|
1034
|
+
userName = me.name || (me.email ? me.email.split("@")[0] : void 0);
|
|
1035
|
+
}
|
|
1036
|
+
} catch {
|
|
1037
|
+
}
|
|
1038
|
+
const orgs = await listOrgs(token);
|
|
1039
|
+
const personal = orgs.find((o) => o.name.endsWith("'s Organization"));
|
|
1040
|
+
const org = personal ?? orgs[0];
|
|
1041
|
+
const orgId = org?.id ?? "";
|
|
1042
|
+
const orgName = org?.name ?? orgId;
|
|
1043
|
+
let savedToken = token;
|
|
1044
|
+
if (orgId) {
|
|
1045
|
+
try {
|
|
1046
|
+
const resp = await fetch(`${DEFAULT_API_URL2}/users/me/tokens`, {
|
|
1047
|
+
method: "POST",
|
|
1048
|
+
headers: {
|
|
1049
|
+
Authorization: `Bearer ${token}`,
|
|
1050
|
+
"Content-Type": "application/json",
|
|
1051
|
+
"X-Activeloop-Org-Id": orgId
|
|
1052
|
+
},
|
|
1053
|
+
body: JSON.stringify({ name: `hivemind-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`, duration: 365 * 24 * 60 * 60, organization_id: orgId })
|
|
1054
|
+
});
|
|
1055
|
+
if (resp.ok) {
|
|
1056
|
+
const data = await resp.json();
|
|
1057
|
+
savedToken = typeof data.token === "string" ? data.token : data.token.token;
|
|
1058
|
+
}
|
|
1059
|
+
} catch {
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
saveCredentials({ token: savedToken, orgId, orgName, userName, apiUrl: DEFAULT_API_URL2, savedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1063
|
+
authPending = false;
|
|
1064
|
+
authUrl = null;
|
|
1065
|
+
justAuthenticated = true;
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
} catch {
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
authPending = false;
|
|
1072
|
+
authUrl = null;
|
|
1073
|
+
})();
|
|
1074
|
+
return code.verification_uri_complete;
|
|
1075
|
+
} catch (err) {
|
|
1076
|
+
authPending = false;
|
|
1077
|
+
throw err;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
var api = null;
|
|
1081
|
+
var sessionsTable = "sessions";
|
|
1082
|
+
var memoryTable = "memory";
|
|
1083
|
+
var captureEnabled = true;
|
|
1084
|
+
var capturedCounts = /* @__PURE__ */ new Map();
|
|
1085
|
+
var fallbackSessionId = crypto.randomUUID();
|
|
1086
|
+
function buildSessionPath(config, sessionId) {
|
|
1087
|
+
return `/sessions/${config.userName}/${config.userName}_${config.orgName}_${config.workspaceId}_${sessionId}.jsonl`;
|
|
1088
|
+
}
|
|
1089
|
+
var RECALL_STOPWORDS = /* @__PURE__ */ new Set([
|
|
1090
|
+
"the",
|
|
1091
|
+
"and",
|
|
1092
|
+
"for",
|
|
1093
|
+
"are",
|
|
1094
|
+
"but",
|
|
1095
|
+
"not",
|
|
1096
|
+
"you",
|
|
1097
|
+
"all",
|
|
1098
|
+
"can",
|
|
1099
|
+
"had",
|
|
1100
|
+
"her",
|
|
1101
|
+
"was",
|
|
1102
|
+
"one",
|
|
1103
|
+
"our",
|
|
1104
|
+
"out",
|
|
1105
|
+
"has",
|
|
1106
|
+
"have",
|
|
1107
|
+
"what",
|
|
1108
|
+
"does",
|
|
1109
|
+
"like",
|
|
1110
|
+
"with",
|
|
1111
|
+
"this",
|
|
1112
|
+
"that",
|
|
1113
|
+
"from",
|
|
1114
|
+
"they",
|
|
1115
|
+
"been",
|
|
1116
|
+
"will",
|
|
1117
|
+
"more",
|
|
1118
|
+
"when",
|
|
1119
|
+
"who",
|
|
1120
|
+
"how",
|
|
1121
|
+
"its",
|
|
1122
|
+
"into",
|
|
1123
|
+
"some",
|
|
1124
|
+
"than",
|
|
1125
|
+
"them",
|
|
1126
|
+
"these",
|
|
1127
|
+
"then",
|
|
1128
|
+
"your",
|
|
1129
|
+
"just",
|
|
1130
|
+
"about",
|
|
1131
|
+
"would",
|
|
1132
|
+
"could",
|
|
1133
|
+
"should",
|
|
1134
|
+
"where",
|
|
1135
|
+
"which",
|
|
1136
|
+
"there",
|
|
1137
|
+
"their",
|
|
1138
|
+
"being",
|
|
1139
|
+
"each",
|
|
1140
|
+
"other"
|
|
1141
|
+
]);
|
|
1142
|
+
function extractKeywords(prompt) {
|
|
1143
|
+
return prompt.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length >= 3 && !RECALL_STOPWORDS.has(w)).slice(0, 4);
|
|
1144
|
+
}
|
|
1145
|
+
function normalizeVirtualPath(p) {
|
|
1146
|
+
if (!p || typeof p !== "string") return "/";
|
|
1147
|
+
const trimmed = p.trim();
|
|
1148
|
+
if (!trimmed || trimmed === "/") return "/";
|
|
1149
|
+
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
1150
|
+
}
|
|
1151
|
+
async function getApi() {
|
|
1152
|
+
if (api) return api;
|
|
1153
|
+
const config = loadConfig();
|
|
1154
|
+
if (!config) {
|
|
1155
|
+
if (!authPending) await requestAuth();
|
|
1156
|
+
return null;
|
|
1157
|
+
}
|
|
1158
|
+
sessionsTable = config.sessionsTableName;
|
|
1159
|
+
memoryTable = config.tableName;
|
|
1160
|
+
api = new DeeplakeApi(config.token, config.apiUrl, config.orgId, config.workspaceId, config.tableName);
|
|
1161
|
+
await api.ensureTable();
|
|
1162
|
+
await api.ensureSessionsTable(sessionsTable);
|
|
1163
|
+
return api;
|
|
1164
|
+
}
|
|
1165
|
+
var src_default = definePluginEntry({
|
|
1166
|
+
id: "hivemind",
|
|
1167
|
+
name: "Hivemind",
|
|
1168
|
+
description: "Cloud-backed shared memory powered by Deeplake",
|
|
1169
|
+
register(pluginApi) {
|
|
1170
|
+
try {
|
|
1171
|
+
pluginApi.registerCommand({
|
|
1172
|
+
name: "hivemind_login",
|
|
1173
|
+
description: "Log in to Hivemind (or switch accounts)",
|
|
1174
|
+
handler: async () => {
|
|
1175
|
+
const existing = loadCredentials();
|
|
1176
|
+
const url = await requestAuth();
|
|
1177
|
+
if (existing?.token) {
|
|
1178
|
+
return {
|
|
1179
|
+
text: `\u2139\uFE0F Currently logged in as ${existing.orgName ?? existing.orgId}.
|
|
1180
|
+
|
|
1181
|
+
To re-authenticate or switch accounts:
|
|
1182
|
+
|
|
1183
|
+
${url}
|
|
1184
|
+
|
|
1185
|
+
After signing in, send another message.`
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
return { text: `\u{1F510} Sign in to activate Hivemind memory:
|
|
1189
|
+
|
|
1190
|
+
${url}
|
|
1191
|
+
|
|
1192
|
+
After signing in, send another message.` };
|
|
1193
|
+
}
|
|
1194
|
+
});
|
|
1195
|
+
pluginApi.registerCommand({
|
|
1196
|
+
name: "hivemind_capture",
|
|
1197
|
+
description: "Toggle conversation capture on/off",
|
|
1198
|
+
handler: async () => {
|
|
1199
|
+
captureEnabled = !captureEnabled;
|
|
1200
|
+
return { text: captureEnabled ? "\u2705 Capture enabled \u2014 conversations will be stored to Hivemind." : "\u23F8\uFE0F Capture paused \u2014 conversations will NOT be stored until you run /hivemind_capture again." };
|
|
1201
|
+
}
|
|
1202
|
+
});
|
|
1203
|
+
pluginApi.registerCommand({
|
|
1204
|
+
name: "hivemind_whoami",
|
|
1205
|
+
description: "Show current Hivemind org and workspace",
|
|
1206
|
+
handler: async () => {
|
|
1207
|
+
const creds2 = loadCredentials();
|
|
1208
|
+
if (!creds2?.token) return { text: "Not logged in. Run /hivemind_login" };
|
|
1209
|
+
return { text: `Org: ${creds2.orgName ?? creds2.orgId}
|
|
1210
|
+
Workspace: ${creds2.workspaceId ?? "default"}` };
|
|
1211
|
+
}
|
|
1212
|
+
});
|
|
1213
|
+
pluginApi.registerCommand({
|
|
1214
|
+
name: "hivemind_orgs",
|
|
1215
|
+
description: "List available organizations",
|
|
1216
|
+
handler: async () => {
|
|
1217
|
+
const creds2 = loadCredentials();
|
|
1218
|
+
if (!creds2?.token) return { text: "Not logged in. Run /hivemind_login" };
|
|
1219
|
+
const orgs = await listOrgs(creds2.token, creds2.apiUrl);
|
|
1220
|
+
if (!orgs.length) return { text: "No organizations found." };
|
|
1221
|
+
const lines = orgs.map((o) => `${o.id === creds2.orgId ? "\u2192 " : " "}${o.name}`);
|
|
1222
|
+
return { text: lines.join("\n") };
|
|
1223
|
+
}
|
|
1224
|
+
});
|
|
1225
|
+
pluginApi.registerCommand({
|
|
1226
|
+
name: "hivemind_switch_org",
|
|
1227
|
+
description: "Switch to a different organization",
|
|
1228
|
+
acceptsArgs: true,
|
|
1229
|
+
handler: async (ctx) => {
|
|
1230
|
+
const creds2 = loadCredentials();
|
|
1231
|
+
if (!creds2?.token) return { text: "Not logged in. Run /hivemind_login" };
|
|
1232
|
+
const target = ctx.args?.trim();
|
|
1233
|
+
if (!target) return { text: "Usage: /hivemind_switch_org <name-or-id>" };
|
|
1234
|
+
const orgs = await listOrgs(creds2.token, creds2.apiUrl);
|
|
1235
|
+
const lc = target.toLowerCase();
|
|
1236
|
+
const match = orgs.find((o) => o.id === target || o.name.toLowerCase() === lc) ?? orgs.find((o) => o.name.toLowerCase().includes(lc) || o.id.toLowerCase().includes(lc));
|
|
1237
|
+
if (!match) {
|
|
1238
|
+
const available = orgs.length ? orgs.map((o) => ` - ${o.name} (id: ${o.id})`).join("\n") : " (none \u2014 your current token has no organization access)";
|
|
1239
|
+
return { text: `Org not found: ${target}
|
|
1240
|
+
|
|
1241
|
+
Available:
|
|
1242
|
+
${available}` };
|
|
1243
|
+
}
|
|
1244
|
+
await switchOrg(match.id, match.name);
|
|
1245
|
+
api = null;
|
|
1246
|
+
return { text: `Switched to org: ${match.name}` };
|
|
1247
|
+
}
|
|
1248
|
+
});
|
|
1249
|
+
pluginApi.registerCommand({
|
|
1250
|
+
name: "hivemind_workspaces",
|
|
1251
|
+
description: "List available workspaces",
|
|
1252
|
+
handler: async () => {
|
|
1253
|
+
const creds2 = loadCredentials();
|
|
1254
|
+
if (!creds2?.token) return { text: "Not logged in. Run /hivemind_login" };
|
|
1255
|
+
const ws = await listWorkspaces(creds2.token, creds2.apiUrl, creds2.orgId);
|
|
1256
|
+
if (!ws.length) return { text: "No workspaces found." };
|
|
1257
|
+
const lines = ws.map((w) => `${w.id === (creds2.workspaceId ?? "default") ? "\u2192 " : " "}${w.name}`);
|
|
1258
|
+
return { text: lines.join("\n") };
|
|
1259
|
+
}
|
|
1260
|
+
});
|
|
1261
|
+
pluginApi.registerCommand({
|
|
1262
|
+
name: "hivemind_switch_workspace",
|
|
1263
|
+
description: "Switch to a different workspace",
|
|
1264
|
+
acceptsArgs: true,
|
|
1265
|
+
handler: async (ctx) => {
|
|
1266
|
+
const creds2 = loadCredentials();
|
|
1267
|
+
if (!creds2?.token) return { text: "Not logged in. Run /hivemind_login" };
|
|
1268
|
+
const target = ctx.args?.trim();
|
|
1269
|
+
if (!target) return { text: "Usage: /hivemind_switch_workspace <name-or-id>" };
|
|
1270
|
+
const ws = await listWorkspaces(creds2.token, creds2.apiUrl, creds2.orgId);
|
|
1271
|
+
const lc = target.toLowerCase();
|
|
1272
|
+
const match = ws.find((w) => w.id === target || w.name.toLowerCase() === lc) ?? ws.find((w) => w.name.toLowerCase().includes(lc) || w.id.toLowerCase().includes(lc));
|
|
1273
|
+
if (!match) {
|
|
1274
|
+
const available = ws.length ? ws.map((w) => ` - ${w.name} (id: ${w.id})`).join("\n") : " (none in current org \u2014 try /hivemind_switch_org first)";
|
|
1275
|
+
return { text: `Workspace not found: ${target}
|
|
1276
|
+
|
|
1277
|
+
Available:
|
|
1278
|
+
${available}` };
|
|
1279
|
+
}
|
|
1280
|
+
await switchWorkspace(match.id);
|
|
1281
|
+
api = null;
|
|
1282
|
+
return { text: `Switched to workspace: ${match.name}` };
|
|
1283
|
+
}
|
|
1284
|
+
});
|
|
1285
|
+
pluginApi.registerCommand({
|
|
1286
|
+
name: "hivemind_setup",
|
|
1287
|
+
description: "Add Hivemind tools to your openclaw allowlist (needed once per install)",
|
|
1288
|
+
handler: async () => {
|
|
1289
|
+
const result = ensureHivemindAllowlisted();
|
|
1290
|
+
if (result.status === "already-set") {
|
|
1291
|
+
return { text: `\u2705 Hivemind tools are already enabled in your allowlist.
|
|
1292
|
+
|
|
1293
|
+
No changes needed \u2014 memory tools are available to the agent.` };
|
|
1294
|
+
}
|
|
1295
|
+
if (result.status === "added") {
|
|
1296
|
+
return { text: `\u2705 Added "hivemind" to your tool allowlist.
|
|
1297
|
+
|
|
1298
|
+
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
|
+
|
|
1300
|
+
Backup of previous config: ${result.backupPath}` };
|
|
1301
|
+
}
|
|
1302
|
+
return { text: `\u26A0\uFE0F Could not update allowlist: ${result.error}
|
|
1303
|
+
|
|
1304
|
+
Manual fix: open ${result.configPath} and add "hivemind" to the "alsoAllow" array under "tools".` };
|
|
1305
|
+
}
|
|
1306
|
+
});
|
|
1307
|
+
pluginApi.registerCommand({
|
|
1308
|
+
name: "hivemind_version",
|
|
1309
|
+
description: "Show the installed Hivemind version and check for updates",
|
|
1310
|
+
handler: async () => {
|
|
1311
|
+
const current = getInstalledVersion();
|
|
1312
|
+
if (!current) return { text: "Could not determine installed version." };
|
|
1313
|
+
try {
|
|
1314
|
+
const res = await fetch(VERSION_URL, { signal: AbortSignal.timeout(3e3) });
|
|
1315
|
+
if (!res.ok) return { text: `Current version: ${current}. Could not check for updates.` };
|
|
1316
|
+
const latest = extractLatestVersion(await res.json());
|
|
1317
|
+
if (!latest) return { text: `Current version: ${current}. Could not parse latest version.` };
|
|
1318
|
+
if (isNewer(latest, current)) {
|
|
1319
|
+
return { text: `\u2B06\uFE0F Update available: ${current} \u2192 ${latest}
|
|
1320
|
+
|
|
1321
|
+
Run /hivemind_update to install it now.` };
|
|
1322
|
+
}
|
|
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
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
});
|
|
1329
|
+
pluginApi.registerCommand({
|
|
1330
|
+
name: "hivemind_update",
|
|
1331
|
+
description: "Install the latest Hivemind version from ClawHub",
|
|
1332
|
+
handler: async () => {
|
|
1333
|
+
const current = getInstalledVersion() ?? "unknown";
|
|
1334
|
+
return {
|
|
1335
|
+
text: `Hivemind v${current} installed. To install the latest:
|
|
1336
|
+
|
|
1337
|
+
\u2022 Ask me in chat: "update hivemind" \u2014 I'll run \`openclaw plugins update hivemind\` via my exec tool.
|
|
1338
|
+
\u2022 Or run in your terminal: \`openclaw plugins update hivemind\`
|
|
1339
|
+
|
|
1340
|
+
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}` };
|
|
1356
|
+
}
|
|
1357
|
+
return {
|
|
1358
|
+
text: result.newValue ? "\u2705 Auto-update is ON. Hivemind will install new versions automatically when the gateway starts." : "\u23F8\uFE0F Auto-update is OFF. Run /hivemind_update manually to install new versions."
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
});
|
|
1362
|
+
pluginApi.registerTool({
|
|
1363
|
+
name: "hivemind_search",
|
|
1364
|
+
label: "Hivemind Search",
|
|
1365
|
+
description: "Search Hivemind shared memory (summaries + past session turns) for keywords, phrases, or regex. Returns matching path + snippet pairs from BOTH the memory and sessions tables. Use this FIRST when the user asks about past work, decisions, people, or anything that might live in memory.",
|
|
1366
|
+
parameters: {
|
|
1367
|
+
type: "object",
|
|
1368
|
+
additionalProperties: false,
|
|
1369
|
+
properties: {
|
|
1370
|
+
query: {
|
|
1371
|
+
type: "string",
|
|
1372
|
+
minLength: 1,
|
|
1373
|
+
description: "Search text. Treated as a literal substring by default; set `regex: true` to use regex metacharacters."
|
|
1374
|
+
},
|
|
1375
|
+
path: {
|
|
1376
|
+
type: "string",
|
|
1377
|
+
description: "Optional virtual path prefix to scope the search, e.g. '/summaries/' or '/sessions/alice/'. Defaults to '/' (all of memory)."
|
|
1378
|
+
},
|
|
1379
|
+
regex: {
|
|
1380
|
+
type: "boolean",
|
|
1381
|
+
description: "If true, `query` is interpreted as a regex. Default false (literal substring)."
|
|
1382
|
+
},
|
|
1383
|
+
ignoreCase: {
|
|
1384
|
+
type: "boolean",
|
|
1385
|
+
description: "Case-insensitive match. Default true."
|
|
1386
|
+
},
|
|
1387
|
+
limit: {
|
|
1388
|
+
type: "integer",
|
|
1389
|
+
minimum: 1,
|
|
1390
|
+
maximum: 100,
|
|
1391
|
+
description: "Max rows returned per table. Default 20."
|
|
1392
|
+
}
|
|
1393
|
+
},
|
|
1394
|
+
required: ["query"]
|
|
1395
|
+
},
|
|
1396
|
+
execute: async (_toolCallId, rawParams) => {
|
|
1397
|
+
const params = rawParams;
|
|
1398
|
+
const dl = await getApi();
|
|
1399
|
+
if (!dl) {
|
|
1400
|
+
return {
|
|
1401
|
+
content: [{ type: "text", text: "Not logged in. Run /hivemind_login first." }]
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
1404
|
+
const targetPath = normalizeVirtualPath(params.path);
|
|
1405
|
+
const grepParams = {
|
|
1406
|
+
pattern: params.query,
|
|
1407
|
+
ignoreCase: params.ignoreCase !== false,
|
|
1408
|
+
wordMatch: false,
|
|
1409
|
+
filesOnly: false,
|
|
1410
|
+
countOnly: false,
|
|
1411
|
+
lineNumber: false,
|
|
1412
|
+
invertMatch: false,
|
|
1413
|
+
fixedString: params.regex !== true
|
|
1414
|
+
};
|
|
1415
|
+
const searchOpts = buildGrepSearchOptions(grepParams, targetPath);
|
|
1416
|
+
searchOpts.limit = Math.min(Math.max(params.limit ?? 20, 1), 100);
|
|
1417
|
+
const t0 = Date.now();
|
|
1418
|
+
try {
|
|
1419
|
+
const rawRows = await searchDeeplakeTables(dl, memoryTable, sessionsTable, searchOpts);
|
|
1420
|
+
const matchedRows = searchOpts.contentScanOnly ? (() => {
|
|
1421
|
+
const re = compileGrepRegex(grepParams);
|
|
1422
|
+
return rawRows.filter((r) => re.test(normalizeContent(r.path, r.content)));
|
|
1423
|
+
})() : rawRows;
|
|
1424
|
+
pluginApi.logger.info?.(`hivemind_search "${params.query.slice(0, 60)}" \u2192 ${matchedRows.length}/${rawRows.length} hits in ${Date.now() - t0}ms`);
|
|
1425
|
+
if (matchedRows.length === 0) {
|
|
1426
|
+
return { content: [{ type: "text", text: `No memory matches for "${params.query}" under ${targetPath}.` }] };
|
|
1427
|
+
}
|
|
1428
|
+
const text = matchedRows.map((r, i) => {
|
|
1429
|
+
const body = normalizeContent(r.path, r.content);
|
|
1430
|
+
return `${i + 1}. ${r.path}
|
|
1431
|
+
${body.slice(0, 500)}`;
|
|
1432
|
+
}).join("\n\n");
|
|
1433
|
+
return { content: [{ type: "text", text }], details: { hits: matchedRows.length, path: targetPath } };
|
|
1434
|
+
} catch (err) {
|
|
1435
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1436
|
+
pluginApi.logger.error(`hivemind_search failed: ${msg}`);
|
|
1437
|
+
return { content: [{ type: "text", text: `Search failed: ${msg}` }] };
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
});
|
|
1441
|
+
pluginApi.registerTool({
|
|
1442
|
+
name: "hivemind_read",
|
|
1443
|
+
label: "Hivemind Read",
|
|
1444
|
+
description: "Read the full content of a specific Hivemind memory path (e.g. '/summaries/alice/abc.md' or '/sessions/alice/alice_org_ws_xyz.jsonl' or '/index.md'). Use this after hivemind_search to drill into a hit, or after hivemind_index to fetch a specific session.",
|
|
1445
|
+
parameters: {
|
|
1446
|
+
type: "object",
|
|
1447
|
+
additionalProperties: false,
|
|
1448
|
+
properties: {
|
|
1449
|
+
path: {
|
|
1450
|
+
type: "string",
|
|
1451
|
+
minLength: 1,
|
|
1452
|
+
description: "Virtual path under /summaries/, /sessions/, or '/index.md' for the memory index."
|
|
1453
|
+
}
|
|
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
|
+
}
|
|
1463
|
+
const virtualPath = normalizeVirtualPath(params.path);
|
|
1464
|
+
try {
|
|
1465
|
+
const content = await readVirtualPathContent(dl, memoryTable, sessionsTable, virtualPath);
|
|
1466
|
+
if (content === null) {
|
|
1467
|
+
return { content: [{ type: "text", text: `No content at ${virtualPath}.` }] };
|
|
1468
|
+
}
|
|
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
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
});
|
|
1501
|
+
pluginApi.registerMemoryCorpusSupplement({
|
|
1502
|
+
search: async ({ query, maxResults }) => {
|
|
1503
|
+
const dl = await getApi();
|
|
1504
|
+
if (!dl) return [];
|
|
1505
|
+
const grepParams = {
|
|
1506
|
+
pattern: query,
|
|
1507
|
+
ignoreCase: true,
|
|
1508
|
+
wordMatch: false,
|
|
1509
|
+
filesOnly: false,
|
|
1510
|
+
countOnly: false,
|
|
1511
|
+
lineNumber: false,
|
|
1512
|
+
invertMatch: false,
|
|
1513
|
+
fixedString: true
|
|
1514
|
+
};
|
|
1515
|
+
const searchOpts = buildGrepSearchOptions(grepParams, "/");
|
|
1516
|
+
searchOpts.limit = Math.min(Math.max(maxResults ?? 10, 1), 50);
|
|
1517
|
+
try {
|
|
1518
|
+
const rows = await searchDeeplakeTables(dl, memoryTable, sessionsTable, searchOpts);
|
|
1519
|
+
return rows.map((r, i) => ({
|
|
1520
|
+
path: r.path,
|
|
1521
|
+
snippet: normalizeContent(r.path, r.content).slice(0, 400),
|
|
1522
|
+
corpus: "hivemind",
|
|
1523
|
+
kind: r.path.startsWith("/summaries/") ? "summary" : "session",
|
|
1524
|
+
score: r.path.startsWith("/summaries/") ? 0.8 - i * 5e-3 : 0.6 - i * 5e-3
|
|
1525
|
+
}));
|
|
1526
|
+
} catch {
|
|
1527
|
+
return [];
|
|
1528
|
+
}
|
|
1529
|
+
},
|
|
1530
|
+
get: async ({ lookup }) => {
|
|
1531
|
+
const dl = await getApi();
|
|
1532
|
+
if (!dl) return null;
|
|
1533
|
+
try {
|
|
1534
|
+
const content = await readVirtualPathContent(dl, memoryTable, sessionsTable, normalizeVirtualPath(lookup));
|
|
1535
|
+
return content === null ? null : { path: lookup, content };
|
|
1536
|
+
} catch {
|
|
1537
|
+
return null;
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
});
|
|
1541
|
+
const config = pluginApi.pluginConfig ?? {};
|
|
1542
|
+
const logger = pluginApi.logger;
|
|
1543
|
+
const hook = (event, handler) => {
|
|
1544
|
+
pluginApi.on(event, handler);
|
|
1545
|
+
};
|
|
1546
|
+
if (config.autoUpdate !== false) {
|
|
1547
|
+
(async () => {
|
|
1548
|
+
try {
|
|
1549
|
+
const current = getInstalledVersion();
|
|
1550
|
+
if (!current) return;
|
|
1551
|
+
const res = await fetch(VERSION_URL, { signal: AbortSignal.timeout(3e3) });
|
|
1552
|
+
if (!res.ok) return;
|
|
1553
|
+
const latest = extractLatestVersion(await res.json());
|
|
1554
|
+
if (!latest || !isNewer(latest, current)) return;
|
|
1555
|
+
pendingUpdate = { current, latest };
|
|
1556
|
+
logger.info?.(`Hivemind update available: ${current} \u2192 ${latest}. Agent will be prompted to install when user asks.`);
|
|
1557
|
+
} catch (err) {
|
|
1558
|
+
logger.error(`Auto-update check failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1559
|
+
}
|
|
1560
|
+
})();
|
|
1561
|
+
}
|
|
1562
|
+
if ('---\nname: hivemind\ndescription: Global team and org memory powered by Activeloop. ALWAYS check BOTH built-in memory AND Hivemind memory when recalling information.\nallowed-tools: hivemind_search, hivemind_read, hivemind_index\n---\n\n# Hivemind Memory\n\nYou have TWO memory sources. ALWAYS check BOTH when the user asks you to recall, remember, or look up ANY information:\n\n1. **Your built-in memory** \u2014 personal per-project notes from the host agent\n2. **Hivemind global memory** \u2014 global memory shared across all sessions, users, and agents in the org, accessed via the tools below\n\n## Memory Structure\n\n```\n/index.md \u2190 START HERE \u2014 table of all sessions\n/summaries/\n <username>/\n <session-id>.md \u2190 AI-generated wiki summary per session\n/sessions/\n <username>/\n <user_org_ws_slug>.jsonl \u2190 raw session data\n```\n\n## How to Search\n\n1. **First**: call `hivemind_index()` \u2014 table of all sessions with dates, projects, descriptions\n2. **If you need details**: call `hivemind_read("/summaries/<username>/<session>.md")`\n3. **If you need raw data**: call `hivemind_read("/sessions/<username>/<file>.jsonl")`\n4. **Keyword search**: call `hivemind_search("keyword")` \u2014 substring search across both summaries and sessions, returns `path:line` hits\n\nDo NOT jump straight to reading raw JSONL files. Always start with `hivemind_index` and summaries.\n\n## Organization Management\n\n- `/hivemind_login` \u2014 sign in via device flow\n- `/hivemind_capture` \u2014 toggle capture on/off (off = no data sent)\n- `/hivemind_whoami` \u2014 show current org and workspace\n- `/hivemind_orgs` \u2014 list organizations\n- `/hivemind_switch_org <name-or-id>` \u2014 switch organization\n- `/hivemind_workspaces` \u2014 list workspaces\n- `/hivemind_switch_workspace <id>` \u2014 switch workspace\n- `/hivemind_version` \u2014 show installed version and check ClawHub for updates\n- `/hivemind_update` \u2014 shows how to install (ask the agent, or run `openclaw plugins update hivemind` in your terminal)\n- `/hivemind_autoupdate [on|off]` \u2014 toggle the agent-facing update nudge (on by default: when a newer version is available, the agent is prompted to install it via `exec` if you ask to update)\n\n## Limits\n\nDo NOT delegate to subagents when reading Hivemind memory. If a tool call returns empty after 2 attempts, skip it and move on. Report what you found rather than exhaustively retrying.\n\n## Getting Started\n\nAfter installing the plugin:\n1. Run `/hivemind_login` to authenticate\n2. Run `/hivemind_setup` to enable the memory tools in your openclaw allowlist (one-time, per install)\n3. Start using memory \u2014 ask questions, the agent automatically captures and searches\n\n## Sharing memory\n\nMultiple agents share memory when users are in the same Activeloop organization.\n'.length > 0) {
|
|
1563
|
+
const allowlistNudge = detectAllowlistMissing() ? "\n\n<hivemind-setup-needed>\nThe user hasn't run /hivemind_setup yet, so hivemind_search, hivemind_read, and hivemind_index are NOT available to you. If they ask about memory and you can't help, tell them to run /hivemind_setup to enable Hivemind memory tools.\n</hivemind-setup-needed>\n" : "";
|
|
1564
|
+
hook("before_prompt_build", async () => {
|
|
1565
|
+
const updateNudge = pendingUpdate ? `
|
|
1566
|
+
|
|
1567
|
+
<hivemind-update-available>
|
|
1568
|
+
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
|
+
</hivemind-update-available>
|
|
1570
|
+
` : "";
|
|
1571
|
+
return {
|
|
1572
|
+
prependSystemContext: allowlistNudge + updateNudge + '\n\n<hivemind-skill>\n---\nname: hivemind\ndescription: Global team and org memory powered by Activeloop. ALWAYS check BOTH built-in memory AND Hivemind memory when recalling information.\nallowed-tools: hivemind_search, hivemind_read, hivemind_index\n---\n\n# Hivemind Memory\n\nYou have TWO memory sources. ALWAYS check BOTH when the user asks you to recall, remember, or look up ANY information:\n\n1. **Your built-in memory** \u2014 personal per-project notes from the host agent\n2. **Hivemind global memory** \u2014 global memory shared across all sessions, users, and agents in the org, accessed via the tools below\n\n## Memory Structure\n\n```\n/index.md \u2190 START HERE \u2014 table of all sessions\n/summaries/\n <username>/\n <session-id>.md \u2190 AI-generated wiki summary per session\n/sessions/\n <username>/\n <user_org_ws_slug>.jsonl \u2190 raw session data\n```\n\n## How to Search\n\n1. **First**: call `hivemind_index()` \u2014 table of all sessions with dates, projects, descriptions\n2. **If you need details**: call `hivemind_read("/summaries/<username>/<session>.md")`\n3. **If you need raw data**: call `hivemind_read("/sessions/<username>/<file>.jsonl")`\n4. **Keyword search**: call `hivemind_search("keyword")` \u2014 substring search across both summaries and sessions, returns `path:line` hits\n\nDo NOT jump straight to reading raw JSONL files. Always start with `hivemind_index` and summaries.\n\n## Organization Management\n\n- `/hivemind_login` \u2014 sign in via device flow\n- `/hivemind_capture` \u2014 toggle capture on/off (off = no data sent)\n- `/hivemind_whoami` \u2014 show current org and workspace\n- `/hivemind_orgs` \u2014 list organizations\n- `/hivemind_switch_org <name-or-id>` \u2014 switch organization\n- `/hivemind_workspaces` \u2014 list workspaces\n- `/hivemind_switch_workspace <id>` \u2014 switch workspace\n- `/hivemind_version` \u2014 show installed version and check ClawHub for updates\n- `/hivemind_update` \u2014 shows how to install (ask the agent, or run `openclaw plugins update hivemind` in your terminal)\n- `/hivemind_autoupdate [on|off]` \u2014 toggle the agent-facing update nudge (on by default: when a newer version is available, the agent is prompted to install it via `exec` if you ask to update)\n\n## Limits\n\nDo NOT delegate to subagents when reading Hivemind memory. If a tool call returns empty after 2 attempts, skip it and move on. Report what you found rather than exhaustively retrying.\n\n## Getting Started\n\nAfter installing the plugin:\n1. Run `/hivemind_login` to authenticate\n2. Run `/hivemind_setup` to enable the memory tools in your openclaw allowlist (one-time, per install)\n3. Start using memory \u2014 ask questions, the agent automatically captures and searches\n\n## Sharing memory\n\nMultiple agents share memory when users are in the same Activeloop organization.\n\n</hivemind-skill>\n'
|
|
1573
|
+
};
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1576
|
+
if (config.autoRecall !== false) {
|
|
1577
|
+
hook("before_agent_start", async (event) => {
|
|
1578
|
+
if (!event.prompt || event.prompt.length < 5) return;
|
|
1579
|
+
try {
|
|
1580
|
+
const dl = await getApi();
|
|
1581
|
+
if (!dl && authUrl) {
|
|
1582
|
+
return {
|
|
1583
|
+
prependContext: `
|
|
1584
|
+
|
|
1585
|
+
IMPORTANT: The user needs to authenticate with Hivemind. Include this link in your reply: ${authUrl}
|
|
1586
|
+
Tell them to click it and sign in, then send another message.
|
|
1587
|
+
`
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
if (!dl) return;
|
|
1591
|
+
if (justAuthenticated) {
|
|
1592
|
+
justAuthenticated = false;
|
|
1593
|
+
const creds2 = loadCredentials();
|
|
1594
|
+
const orgName = creds2?.orgName ?? creds2?.orgId ?? "unknown";
|
|
1595
|
+
return { prependContext: `
|
|
1596
|
+
|
|
1597
|
+
\u{1F41D} Welcome to Hivemind!
|
|
1598
|
+
|
|
1599
|
+
Current org: ${orgName}
|
|
1600
|
+
|
|
1601
|
+
Your agents now share memory across sessions, teammates, and machines.
|
|
1602
|
+
|
|
1603
|
+
Get started:
|
|
1604
|
+
1. Verify sync: spin up multiple sessions and confirm agents share context
|
|
1605
|
+
2. Invite a teammate: ask the agent to add them over email
|
|
1606
|
+
3. Switch orgs: ask the agent to list or switch your organizations
|
|
1607
|
+
|
|
1608
|
+
One brain for every agent on your team.
|
|
1609
|
+
` };
|
|
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
|
+
}
|
|
1666
|
+
if (!text.trim()) continue;
|
|
1667
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
1668
|
+
const entry = {
|
|
1669
|
+
id: crypto.randomUUID(),
|
|
1670
|
+
type: msg.role === "user" ? "user_message" : "assistant_message",
|
|
1671
|
+
session_id: sid,
|
|
1672
|
+
content: text,
|
|
1673
|
+
timestamp: ts
|
|
1674
|
+
};
|
|
1675
|
+
const line = JSON.stringify(entry);
|
|
1676
|
+
const jsonForSql = line.replace(/'/g, "''");
|
|
1677
|
+
const insertSql = `INSERT INTO "${sessionsTable}" (id, path, filename, message, author, size_bytes, project, description, agent, creation_date, last_update_date) VALUES ('${crypto.randomUUID()}', '${sqlStr(sessionPath)}', '${sqlStr(filename)}', '${jsonForSql}'::jsonb, '${sqlStr(cfg.userName)}', ${Buffer.byteLength(line, "utf-8")}, '${sqlStr(projectName)}', '${sqlStr(msg.role)}', 'openclaw', '${ts}', '${ts}')`;
|
|
1678
|
+
try {
|
|
1679
|
+
await dl.query(insertSql);
|
|
1680
|
+
} catch (e) {
|
|
1681
|
+
if (e.message?.includes("permission denied") || e.message?.includes("does not exist")) {
|
|
1682
|
+
await dl.ensureSessionsTable(sessionsTable);
|
|
1683
|
+
await dl.query(insertSql);
|
|
1684
|
+
} else {
|
|
1685
|
+
throw e;
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
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
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
checkForUpdate(logger).catch(() => {
|
|
1705
|
+
});
|
|
1706
|
+
logger.info?.("Hivemind plugin registered");
|
|
1707
|
+
} catch (err) {
|
|
1708
|
+
pluginApi.logger?.error?.(`Hivemind register failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
});
|
|
1712
|
+
export {
|
|
1713
|
+
src_default as default
|
|
1714
|
+
};
|