@deeplake/hivemind 0.6.47 → 0.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +158 -51
- package/bundle/cli.js +4103 -282
- package/codex/bundle/capture.js +510 -90
- package/codex/bundle/commands/auth-login.js +219 -72
- package/codex/bundle/embeddings/embed-daemon.js +243 -0
- package/codex/bundle/pre-tool-use.js +713 -108
- package/codex/bundle/session-start-setup.js +209 -58
- package/codex/bundle/session-start.js +40 -11
- package/codex/bundle/shell/deeplake-shell.js +679 -112
- package/codex/bundle/stop.js +477 -59
- package/codex/bundle/wiki-worker.js +312 -11
- package/cursor/bundle/capture.js +768 -57
- package/cursor/bundle/commands/auth-login.js +219 -72
- package/cursor/bundle/embeddings/embed-daemon.js +243 -0
- package/cursor/bundle/pre-tool-use.js +1684 -0
- package/cursor/bundle/session-end.js +223 -2
- package/cursor/bundle/session-start.js +209 -57
- package/cursor/bundle/shell/deeplake-shell.js +679 -112
- package/cursor/bundle/wiki-worker.js +571 -0
- package/hermes/bundle/capture.js +1194 -0
- package/hermes/bundle/commands/auth-login.js +1009 -0
- package/hermes/bundle/embeddings/embed-daemon.js +243 -0
- package/hermes/bundle/package.json +1 -0
- package/hermes/bundle/pre-tool-use.js +1681 -0
- package/hermes/bundle/session-end.js +265 -0
- package/hermes/bundle/session-start.js +655 -0
- package/hermes/bundle/shell/deeplake-shell.js +69905 -0
- package/hermes/bundle/wiki-worker.js +572 -0
- package/mcp/bundle/server.js +289 -69
- package/openclaw/dist/chunks/auth-creds-AEKS6D3P.js +14 -0
- package/openclaw/dist/chunks/chunk-SRCBBT4H.js +37 -0
- package/openclaw/dist/chunks/config-G23NI5TV.js +33 -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 +752 -702
- package/openclaw/openclaw.plugin.json +1 -1
- package/openclaw/package.json +1 -1
- package/package.json +7 -3
- package/pi/extension-source/hivemind.ts +807 -0
package/cursor/bundle/capture.js
CHANGED
|
@@ -1,4 +1,56 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// dist/src/index-marker-store.js
|
|
13
|
+
var index_marker_store_exports = {};
|
|
14
|
+
__export(index_marker_store_exports, {
|
|
15
|
+
buildIndexMarkerPath: () => buildIndexMarkerPath,
|
|
16
|
+
getIndexMarkerDir: () => getIndexMarkerDir,
|
|
17
|
+
hasFreshIndexMarker: () => hasFreshIndexMarker,
|
|
18
|
+
writeIndexMarker: () => writeIndexMarker
|
|
19
|
+
});
|
|
20
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
21
|
+
import { join as join3 } from "node:path";
|
|
22
|
+
import { tmpdir } from "node:os";
|
|
23
|
+
function getIndexMarkerDir() {
|
|
24
|
+
return process.env.HIVEMIND_INDEX_MARKER_DIR ?? join3(tmpdir(), "hivemind-deeplake-indexes");
|
|
25
|
+
}
|
|
26
|
+
function buildIndexMarkerPath(workspaceId, orgId, table, suffix) {
|
|
27
|
+
const markerKey = [workspaceId, orgId, table, suffix].join("__").replace(/[^a-zA-Z0-9_.-]/g, "_");
|
|
28
|
+
return join3(getIndexMarkerDir(), `${markerKey}.json`);
|
|
29
|
+
}
|
|
30
|
+
function hasFreshIndexMarker(markerPath) {
|
|
31
|
+
if (!existsSync2(markerPath))
|
|
32
|
+
return false;
|
|
33
|
+
try {
|
|
34
|
+
const raw = JSON.parse(readFileSync2(markerPath, "utf-8"));
|
|
35
|
+
const updatedAt = raw.updatedAt ? new Date(raw.updatedAt).getTime() : NaN;
|
|
36
|
+
if (!Number.isFinite(updatedAt) || Date.now() - updatedAt > INDEX_MARKER_TTL_MS)
|
|
37
|
+
return false;
|
|
38
|
+
return true;
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function writeIndexMarker(markerPath) {
|
|
44
|
+
mkdirSync(getIndexMarkerDir(), { recursive: true });
|
|
45
|
+
writeFileSync(markerPath, JSON.stringify({ updatedAt: (/* @__PURE__ */ new Date()).toISOString() }), "utf-8");
|
|
46
|
+
}
|
|
47
|
+
var INDEX_MARKER_TTL_MS;
|
|
48
|
+
var init_index_marker_store = __esm({
|
|
49
|
+
"dist/src/index-marker-store.js"() {
|
|
50
|
+
"use strict";
|
|
51
|
+
INDEX_MARKER_TTL_MS = Number(process.env.HIVEMIND_INDEX_MARKER_TTL_MS ?? 6 * 60 * 6e4);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
2
54
|
|
|
3
55
|
// dist/src/utils/stdin.js
|
|
4
56
|
function readStdin() {
|
|
@@ -51,9 +103,6 @@ function loadConfig() {
|
|
|
51
103
|
|
|
52
104
|
// dist/src/deeplake-api.js
|
|
53
105
|
import { randomUUID } from "node:crypto";
|
|
54
|
-
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
55
|
-
import { join as join3 } from "node:path";
|
|
56
|
-
import { tmpdir } from "node:os";
|
|
57
106
|
|
|
58
107
|
// dist/src/utils/debug.js
|
|
59
108
|
import { appendFileSync } from "node:fs";
|
|
@@ -61,6 +110,9 @@ import { join as join2 } from "node:path";
|
|
|
61
110
|
import { homedir as homedir2 } from "node:os";
|
|
62
111
|
var DEBUG = process.env.HIVEMIND_DEBUG === "1";
|
|
63
112
|
var LOG = join2(homedir2(), ".deeplake", "hook-debug.log");
|
|
113
|
+
function utcTimestamp(d = /* @__PURE__ */ new Date()) {
|
|
114
|
+
return d.toISOString().replace("T", " ").slice(0, 19) + " UTC";
|
|
115
|
+
}
|
|
64
116
|
function log(tag, msg) {
|
|
65
117
|
if (!DEBUG)
|
|
66
118
|
return;
|
|
@@ -73,7 +125,26 @@ function sqlStr(value) {
|
|
|
73
125
|
return value.replace(/\\/g, "\\\\").replace(/'/g, "''").replace(/\0/g, "").replace(/[\x01-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
|
|
74
126
|
}
|
|
75
127
|
|
|
128
|
+
// dist/src/embeddings/columns.js
|
|
129
|
+
var SUMMARY_EMBEDDING_COL = "summary_embedding";
|
|
130
|
+
var MESSAGE_EMBEDDING_COL = "message_embedding";
|
|
131
|
+
|
|
132
|
+
// dist/src/utils/client-header.js
|
|
133
|
+
var DEEPLAKE_CLIENT_HEADER = "X-Deeplake-Client";
|
|
134
|
+
function deeplakeClientValue() {
|
|
135
|
+
return "hivemind";
|
|
136
|
+
}
|
|
137
|
+
function deeplakeClientHeader() {
|
|
138
|
+
return { [DEEPLAKE_CLIENT_HEADER]: deeplakeClientValue() };
|
|
139
|
+
}
|
|
140
|
+
|
|
76
141
|
// dist/src/deeplake-api.js
|
|
142
|
+
var indexMarkerStorePromise = null;
|
|
143
|
+
function getIndexMarkerStore() {
|
|
144
|
+
if (!indexMarkerStorePromise)
|
|
145
|
+
indexMarkerStorePromise = Promise.resolve().then(() => (init_index_marker_store(), index_marker_store_exports));
|
|
146
|
+
return indexMarkerStorePromise;
|
|
147
|
+
}
|
|
77
148
|
var log2 = (msg) => log("sdk", msg);
|
|
78
149
|
function summarizeSql(sql, maxLen = 220) {
|
|
79
150
|
const compact = sql.replace(/\s+/g, " ").trim();
|
|
@@ -93,7 +164,6 @@ var MAX_RETRIES = 3;
|
|
|
93
164
|
var BASE_DELAY_MS = 500;
|
|
94
165
|
var MAX_CONCURRENCY = 5;
|
|
95
166
|
var QUERY_TIMEOUT_MS = Number(process.env.HIVEMIND_QUERY_TIMEOUT_MS ?? 1e4);
|
|
96
|
-
var INDEX_MARKER_TTL_MS = Number(process.env.HIVEMIND_INDEX_MARKER_TTL_MS ?? 6 * 60 * 6e4);
|
|
97
167
|
function sleep(ms) {
|
|
98
168
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
99
169
|
}
|
|
@@ -113,9 +183,6 @@ function isTransientHtml403(text) {
|
|
|
113
183
|
const body = text.toLowerCase();
|
|
114
184
|
return body.includes("<html") || body.includes("403 forbidden") || body.includes("cloudflare") || body.includes("nginx");
|
|
115
185
|
}
|
|
116
|
-
function getIndexMarkerDir() {
|
|
117
|
-
return process.env.HIVEMIND_INDEX_MARKER_DIR ?? join3(tmpdir(), "hivemind-deeplake-indexes");
|
|
118
|
-
}
|
|
119
186
|
var Semaphore = class {
|
|
120
187
|
max;
|
|
121
188
|
waiting = [];
|
|
@@ -184,7 +251,8 @@ var DeeplakeApi = class {
|
|
|
184
251
|
headers: {
|
|
185
252
|
Authorization: `Bearer ${this.token}`,
|
|
186
253
|
"Content-Type": "application/json",
|
|
187
|
-
"X-Activeloop-Org-Id": this.orgId
|
|
254
|
+
"X-Activeloop-Org-Id": this.orgId,
|
|
255
|
+
...deeplakeClientHeader()
|
|
188
256
|
},
|
|
189
257
|
signal,
|
|
190
258
|
body: JSON.stringify({ query: sql })
|
|
@@ -211,7 +279,8 @@ var DeeplakeApi = class {
|
|
|
211
279
|
}
|
|
212
280
|
const text = await resp.text().catch(() => "");
|
|
213
281
|
const retryable403 = isSessionInsertQuery(sql) && (resp.status === 401 || resp.status === 403 && (text.length === 0 || isTransientHtml403(text)));
|
|
214
|
-
|
|
282
|
+
const alreadyExists = resp.status === 500 && isDuplicateIndexError(text);
|
|
283
|
+
if (!alreadyExists && attempt < MAX_RETRIES && (RETRYABLE_CODES.has(resp.status) || retryable403)) {
|
|
215
284
|
const delay = BASE_DELAY_MS * Math.pow(2, attempt) + Math.random() * 200;
|
|
216
285
|
log2(`query retry ${attempt + 1}/${MAX_RETRIES} (${resp.status}) in ${delay.toFixed(0)}ms`);
|
|
217
286
|
await sleep(delay);
|
|
@@ -245,7 +314,7 @@ var DeeplakeApi = class {
|
|
|
245
314
|
const lud = row.lastUpdateDate ?? ts;
|
|
246
315
|
const exists = await this.query(`SELECT path FROM "${this.tableName}" WHERE path = '${sqlStr(row.path)}' LIMIT 1`);
|
|
247
316
|
if (exists.length > 0) {
|
|
248
|
-
let setClauses = `summary = E'${sqlStr(row.contentText)}', mime_type = '${sqlStr(row.mimeType)}', size_bytes = ${row.sizeBytes}, last_update_date = '${lud}'`;
|
|
317
|
+
let setClauses = `summary = E'${sqlStr(row.contentText)}', ${SUMMARY_EMBEDDING_COL} = NULL, mime_type = '${sqlStr(row.mimeType)}', size_bytes = ${row.sizeBytes}, last_update_date = '${lud}'`;
|
|
249
318
|
if (row.project !== void 0)
|
|
250
319
|
setClauses += `, project = '${sqlStr(row.project)}'`;
|
|
251
320
|
if (row.description !== void 0)
|
|
@@ -253,8 +322,8 @@ var DeeplakeApi = class {
|
|
|
253
322
|
await this.query(`UPDATE "${this.tableName}" SET ${setClauses} WHERE path = '${sqlStr(row.path)}'`);
|
|
254
323
|
} else {
|
|
255
324
|
const id = randomUUID();
|
|
256
|
-
let cols =
|
|
257
|
-
let vals = `'${id}', '${sqlStr(row.path)}', '${sqlStr(row.filename)}', E'${sqlStr(row.contentText)}', '${sqlStr(row.mimeType)}', ${row.sizeBytes}, '${cd}', '${lud}'`;
|
|
325
|
+
let cols = `id, path, filename, summary, ${SUMMARY_EMBEDDING_COL}, mime_type, size_bytes, creation_date, last_update_date`;
|
|
326
|
+
let vals = `'${id}', '${sqlStr(row.path)}', '${sqlStr(row.filename)}', E'${sqlStr(row.contentText)}', NULL, '${sqlStr(row.mimeType)}', ${row.sizeBytes}, '${cd}', '${lud}'`;
|
|
258
327
|
if (row.project !== void 0) {
|
|
259
328
|
cols += ", project";
|
|
260
329
|
vals += `, '${sqlStr(row.project)}'`;
|
|
@@ -279,48 +348,83 @@ var DeeplakeApi = class {
|
|
|
279
348
|
buildLookupIndexName(table, suffix) {
|
|
280
349
|
return `idx_${table}_${suffix}`.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
281
350
|
}
|
|
282
|
-
getLookupIndexMarkerPath(table, suffix) {
|
|
283
|
-
const markerKey = [
|
|
284
|
-
this.workspaceId,
|
|
285
|
-
this.orgId,
|
|
286
|
-
table,
|
|
287
|
-
suffix
|
|
288
|
-
].join("__").replace(/[^a-zA-Z0-9_.-]/g, "_");
|
|
289
|
-
return join3(getIndexMarkerDir(), `${markerKey}.json`);
|
|
290
|
-
}
|
|
291
|
-
hasFreshLookupIndexMarker(table, suffix) {
|
|
292
|
-
const markerPath = this.getLookupIndexMarkerPath(table, suffix);
|
|
293
|
-
if (!existsSync2(markerPath))
|
|
294
|
-
return false;
|
|
295
|
-
try {
|
|
296
|
-
const raw = JSON.parse(readFileSync2(markerPath, "utf-8"));
|
|
297
|
-
const updatedAt = raw.updatedAt ? new Date(raw.updatedAt).getTime() : NaN;
|
|
298
|
-
if (!Number.isFinite(updatedAt) || Date.now() - updatedAt > INDEX_MARKER_TTL_MS)
|
|
299
|
-
return false;
|
|
300
|
-
return true;
|
|
301
|
-
} catch {
|
|
302
|
-
return false;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
markLookupIndexReady(table, suffix) {
|
|
306
|
-
mkdirSync(getIndexMarkerDir(), { recursive: true });
|
|
307
|
-
writeFileSync(this.getLookupIndexMarkerPath(table, suffix), JSON.stringify({ updatedAt: (/* @__PURE__ */ new Date()).toISOString() }), "utf-8");
|
|
308
|
-
}
|
|
309
351
|
async ensureLookupIndex(table, suffix, columnsSql) {
|
|
310
|
-
|
|
352
|
+
const markers = await getIndexMarkerStore();
|
|
353
|
+
const markerPath = markers.buildIndexMarkerPath(this.workspaceId, this.orgId, table, suffix);
|
|
354
|
+
if (markers.hasFreshIndexMarker(markerPath))
|
|
311
355
|
return;
|
|
312
356
|
const indexName = this.buildLookupIndexName(table, suffix);
|
|
313
357
|
try {
|
|
314
358
|
await this.query(`CREATE INDEX IF NOT EXISTS "${indexName}" ON "${table}" ${columnsSql}`);
|
|
315
|
-
|
|
359
|
+
markers.writeIndexMarker(markerPath);
|
|
316
360
|
} catch (e) {
|
|
317
361
|
if (isDuplicateIndexError(e)) {
|
|
318
|
-
|
|
362
|
+
markers.writeIndexMarker(markerPath);
|
|
319
363
|
return;
|
|
320
364
|
}
|
|
321
365
|
log2(`index "${indexName}" skipped: ${e.message}`);
|
|
322
366
|
}
|
|
323
367
|
}
|
|
368
|
+
/**
|
|
369
|
+
* Ensure a vector column exists on the given table.
|
|
370
|
+
*
|
|
371
|
+
* The previous implementation always issued `ALTER TABLE ADD COLUMN IF NOT
|
|
372
|
+
* EXISTS …` on every SessionStart. On a long-running workspace that's
|
|
373
|
+
* already migrated, every call returns 500 "Column already exists" — noisy
|
|
374
|
+
* in the log and a wasted round-trip. Worse, the very first call after the
|
|
375
|
+
* column is genuinely added triggers Deeplake's post-ALTER `vector::at`
|
|
376
|
+
* window (~30s) during which subsequent INSERTs fail; minimising the
|
|
377
|
+
* number of ALTER calls minimises exposure to that window.
|
|
378
|
+
*
|
|
379
|
+
* New flow:
|
|
380
|
+
* 1. Check the local marker file (mirrors ensureLookupIndex). If fresh,
|
|
381
|
+
* return — zero network calls.
|
|
382
|
+
* 2. SELECT 1 FROM information_schema.columns WHERE table_name = T AND
|
|
383
|
+
* column_name = C. Read-only, idempotent, can't tickle the post-ALTER
|
|
384
|
+
* bug. If the column is present → mark + return.
|
|
385
|
+
* 3. Only if step 2 says the column is missing, fall back to ALTER ADD
|
|
386
|
+
* COLUMN IF NOT EXISTS. Mark on success, also mark if Deeplake reports
|
|
387
|
+
* "already exists" (race: another client added it between our SELECT
|
|
388
|
+
* and ALTER).
|
|
389
|
+
*
|
|
390
|
+
* Marker uses the same dir / TTL as ensureLookupIndex so both schema
|
|
391
|
+
* caches share an opt-out (HIVEMIND_INDEX_MARKER_DIR) and a TTL knob.
|
|
392
|
+
*/
|
|
393
|
+
async ensureEmbeddingColumn(table, column) {
|
|
394
|
+
await this.ensureColumn(table, column, "FLOAT4[]");
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Generic marker-gated column migration. Same SELECT-then-ALTER flow as
|
|
398
|
+
* ensureEmbeddingColumn, parameterized by SQL type so it can patch up any
|
|
399
|
+
* column that was added to the schema after the table was originally
|
|
400
|
+
* created. Used today for `summary_embedding`, `message_embedding`, and
|
|
401
|
+
* the `agent` column (added 2026-04-11) — the latter has no fallback if
|
|
402
|
+
* a user upgraded over a pre-2026-04-11 table, so every INSERT fails
|
|
403
|
+
* with `column "agent" does not exist`.
|
|
404
|
+
*/
|
|
405
|
+
async ensureColumn(table, column, sqlType) {
|
|
406
|
+
const markers = await getIndexMarkerStore();
|
|
407
|
+
const markerPath = markers.buildIndexMarkerPath(this.workspaceId, this.orgId, table, `col_${column}`);
|
|
408
|
+
if (markers.hasFreshIndexMarker(markerPath))
|
|
409
|
+
return;
|
|
410
|
+
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`;
|
|
411
|
+
const rows = await this.query(colCheck);
|
|
412
|
+
if (rows.length > 0) {
|
|
413
|
+
markers.writeIndexMarker(markerPath);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
try {
|
|
417
|
+
await this.query(`ALTER TABLE "${table}" ADD COLUMN ${column} ${sqlType}`);
|
|
418
|
+
} catch (e) {
|
|
419
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
420
|
+
if (!/already exists/i.test(msg))
|
|
421
|
+
throw e;
|
|
422
|
+
const recheck = await this.query(colCheck);
|
|
423
|
+
if (recheck.length === 0)
|
|
424
|
+
throw e;
|
|
425
|
+
}
|
|
426
|
+
markers.writeIndexMarker(markerPath);
|
|
427
|
+
}
|
|
324
428
|
/** List all tables in the workspace (with retry). */
|
|
325
429
|
async listTables(forceRefresh = false) {
|
|
326
430
|
if (!forceRefresh && this._tablesCache)
|
|
@@ -336,7 +440,8 @@ var DeeplakeApi = class {
|
|
|
336
440
|
const resp = await fetch(`${this.apiUrl}/workspaces/${this.workspaceId}/tables`, {
|
|
337
441
|
headers: {
|
|
338
442
|
Authorization: `Bearer ${this.token}`,
|
|
339
|
-
"X-Activeloop-Org-Id": this.orgId
|
|
443
|
+
"X-Activeloop-Org-Id": this.orgId,
|
|
444
|
+
...deeplakeClientHeader()
|
|
340
445
|
}
|
|
341
446
|
});
|
|
342
447
|
if (resp.ok) {
|
|
@@ -361,28 +466,60 @@ var DeeplakeApi = class {
|
|
|
361
466
|
}
|
|
362
467
|
return { tables: [], cacheable: false };
|
|
363
468
|
}
|
|
469
|
+
/**
|
|
470
|
+
* Run a `CREATE TABLE` with an extra outer retry budget. The base
|
|
471
|
+
* `query()` already retries 3 times on fetch errors (~3.5s total), but a
|
|
472
|
+
* failed CREATE is permanent corruption — every subsequent SELECT against
|
|
473
|
+
* the missing table fails. Wrapping in an outer loop with longer backoff
|
|
474
|
+
* (2s, 5s, then 10s) gives us ~17s of reach across transient network
|
|
475
|
+
* blips before giving up. Failures still propagate; getApi() resets its
|
|
476
|
+
* cache on init failure (openclaw plugin) so the next call retries the
|
|
477
|
+
* whole init flow.
|
|
478
|
+
*/
|
|
479
|
+
async createTableWithRetry(sql, label) {
|
|
480
|
+
const OUTER_BACKOFFS_MS = [2e3, 5e3, 1e4];
|
|
481
|
+
let lastErr = null;
|
|
482
|
+
for (let attempt = 0; attempt <= OUTER_BACKOFFS_MS.length; attempt++) {
|
|
483
|
+
try {
|
|
484
|
+
await this.query(sql);
|
|
485
|
+
return;
|
|
486
|
+
} catch (err) {
|
|
487
|
+
lastErr = err;
|
|
488
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
489
|
+
log2(`CREATE TABLE "${label}" attempt ${attempt + 1}/${OUTER_BACKOFFS_MS.length + 1} failed: ${msg}`);
|
|
490
|
+
if (attempt < OUTER_BACKOFFS_MS.length) {
|
|
491
|
+
await sleep(OUTER_BACKOFFS_MS[attempt]);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
throw lastErr;
|
|
496
|
+
}
|
|
364
497
|
/** Create the memory table if it doesn't already exist. Migrate columns on existing tables. */
|
|
365
498
|
async ensureTable(name) {
|
|
366
499
|
const tbl = name ?? this.tableName;
|
|
367
500
|
const tables = await this.listTables();
|
|
368
501
|
if (!tables.includes(tbl)) {
|
|
369
502
|
log2(`table "${tbl}" not found, creating`);
|
|
370
|
-
await this.
|
|
503
|
+
await this.createTableWithRetry(`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`, tbl);
|
|
371
504
|
log2(`table "${tbl}" created`);
|
|
372
505
|
if (!tables.includes(tbl))
|
|
373
506
|
this._tablesCache = [...tables, tbl];
|
|
374
507
|
}
|
|
508
|
+
await this.ensureEmbeddingColumn(tbl, SUMMARY_EMBEDDING_COL);
|
|
509
|
+
await this.ensureColumn(tbl, "agent", "TEXT NOT NULL DEFAULT ''");
|
|
375
510
|
}
|
|
376
511
|
/** Create the sessions table (uses JSONB for message since every row is a JSON event). */
|
|
377
512
|
async ensureSessionsTable(name) {
|
|
378
513
|
const tables = await this.listTables();
|
|
379
514
|
if (!tables.includes(name)) {
|
|
380
515
|
log2(`table "${name}" not found, creating`);
|
|
381
|
-
await this.
|
|
516
|
+
await this.createTableWithRetry(`CREATE TABLE IF NOT EXISTS "${name}" (id TEXT NOT NULL DEFAULT '', path TEXT NOT NULL DEFAULT '', filename TEXT NOT NULL DEFAULT '', message JSONB, message_embedding FLOAT4[], author TEXT NOT NULL DEFAULT '', mime_type TEXT NOT NULL DEFAULT 'application/json', size_bytes BIGINT NOT NULL DEFAULT 0, project TEXT NOT NULL DEFAULT '', description TEXT NOT NULL DEFAULT '', agent TEXT NOT NULL DEFAULT '', creation_date TEXT NOT NULL DEFAULT '', last_update_date TEXT NOT NULL DEFAULT '') USING deeplake`, name);
|
|
382
517
|
log2(`table "${name}" created`);
|
|
383
518
|
if (!tables.includes(name))
|
|
384
519
|
this._tablesCache = [...tables, name];
|
|
385
520
|
}
|
|
521
|
+
await this.ensureEmbeddingColumn(name, MESSAGE_EMBEDDING_COL);
|
|
522
|
+
await this.ensureColumn(name, "agent", "TEXT NOT NULL DEFAULT ''");
|
|
386
523
|
await this.ensureLookupIndex(name, "path_creation_date", `("path", "creation_date")`);
|
|
387
524
|
}
|
|
388
525
|
};
|
|
@@ -393,8 +530,547 @@ function buildSessionPath(config, sessionId) {
|
|
|
393
530
|
return `/sessions/${config.userName}/${config.userName}_${config.orgName}_${workspace}_${sessionId}.jsonl`;
|
|
394
531
|
}
|
|
395
532
|
|
|
533
|
+
// dist/src/embeddings/client.js
|
|
534
|
+
import { connect } from "node:net";
|
|
535
|
+
import { spawn } from "node:child_process";
|
|
536
|
+
import { openSync, closeSync, writeSync, unlinkSync, existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
537
|
+
import { homedir as homedir3 } from "node:os";
|
|
538
|
+
import { join as join4 } from "node:path";
|
|
539
|
+
|
|
540
|
+
// dist/src/embeddings/protocol.js
|
|
541
|
+
var DEFAULT_SOCKET_DIR = "/tmp";
|
|
542
|
+
var DEFAULT_IDLE_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
543
|
+
var DEFAULT_CLIENT_TIMEOUT_MS = 2e3;
|
|
544
|
+
function socketPathFor(uid, dir = DEFAULT_SOCKET_DIR) {
|
|
545
|
+
return `${dir}/hivemind-embed-${uid}.sock`;
|
|
546
|
+
}
|
|
547
|
+
function pidPathFor(uid, dir = DEFAULT_SOCKET_DIR) {
|
|
548
|
+
return `${dir}/hivemind-embed-${uid}.pid`;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// dist/src/embeddings/client.js
|
|
552
|
+
var SHARED_DAEMON_PATH = join4(homedir3(), ".hivemind", "embed-deps", "embed-daemon.js");
|
|
553
|
+
var log3 = (m) => log("embed-client", m);
|
|
554
|
+
function getUid() {
|
|
555
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
|
|
556
|
+
return uid !== void 0 ? String(uid) : process.env.USER ?? "default";
|
|
557
|
+
}
|
|
558
|
+
var EmbedClient = class {
|
|
559
|
+
socketPath;
|
|
560
|
+
pidPath;
|
|
561
|
+
timeoutMs;
|
|
562
|
+
daemonEntry;
|
|
563
|
+
autoSpawn;
|
|
564
|
+
spawnWaitMs;
|
|
565
|
+
nextId = 0;
|
|
566
|
+
constructor(opts = {}) {
|
|
567
|
+
const uid = getUid();
|
|
568
|
+
const dir = opts.socketDir ?? "/tmp";
|
|
569
|
+
this.socketPath = socketPathFor(uid, dir);
|
|
570
|
+
this.pidPath = pidPathFor(uid, dir);
|
|
571
|
+
this.timeoutMs = opts.timeoutMs ?? DEFAULT_CLIENT_TIMEOUT_MS;
|
|
572
|
+
this.daemonEntry = opts.daemonEntry ?? process.env.HIVEMIND_EMBED_DAEMON ?? (existsSync3(SHARED_DAEMON_PATH) ? SHARED_DAEMON_PATH : void 0);
|
|
573
|
+
this.autoSpawn = opts.autoSpawn ?? true;
|
|
574
|
+
this.spawnWaitMs = opts.spawnWaitMs ?? 5e3;
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Returns an embedding vector, or null on timeout/failure. Hooks MUST treat
|
|
578
|
+
* null as "skip embedding column" — never block the write path on us.
|
|
579
|
+
*
|
|
580
|
+
* Fire-and-forget spawn on miss: if the daemon isn't up, this call returns
|
|
581
|
+
* null AND kicks off a background spawn. The next call finds a ready daemon.
|
|
582
|
+
*/
|
|
583
|
+
async embed(text, kind = "document") {
|
|
584
|
+
let sock;
|
|
585
|
+
try {
|
|
586
|
+
sock = await this.connectOnce();
|
|
587
|
+
} catch {
|
|
588
|
+
if (this.autoSpawn)
|
|
589
|
+
this.trySpawnDaemon();
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
try {
|
|
593
|
+
const id = String(++this.nextId);
|
|
594
|
+
const req = { op: "embed", id, kind, text };
|
|
595
|
+
const resp = await this.sendAndWait(sock, req);
|
|
596
|
+
if (resp.error || !("embedding" in resp) || !resp.embedding) {
|
|
597
|
+
log3(`embed err: ${resp.error ?? "no embedding"}`);
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
return resp.embedding;
|
|
601
|
+
} catch (e) {
|
|
602
|
+
const err = e instanceof Error ? e.message : String(e);
|
|
603
|
+
log3(`embed failed: ${err}`);
|
|
604
|
+
return null;
|
|
605
|
+
} finally {
|
|
606
|
+
try {
|
|
607
|
+
sock.end();
|
|
608
|
+
} catch {
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Wait up to spawnWaitMs for the daemon to accept connections, spawning if
|
|
614
|
+
* necessary. Meant for SessionStart / long-running batches — not the hot path.
|
|
615
|
+
*/
|
|
616
|
+
async warmup() {
|
|
617
|
+
try {
|
|
618
|
+
const s = await this.connectOnce();
|
|
619
|
+
s.end();
|
|
620
|
+
return true;
|
|
621
|
+
} catch {
|
|
622
|
+
if (!this.autoSpawn)
|
|
623
|
+
return false;
|
|
624
|
+
this.trySpawnDaemon();
|
|
625
|
+
try {
|
|
626
|
+
const s = await this.waitForSocket();
|
|
627
|
+
s.end();
|
|
628
|
+
return true;
|
|
629
|
+
} catch {
|
|
630
|
+
return false;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
connectOnce() {
|
|
635
|
+
return new Promise((resolve, reject) => {
|
|
636
|
+
const sock = connect(this.socketPath);
|
|
637
|
+
const to = setTimeout(() => {
|
|
638
|
+
sock.destroy();
|
|
639
|
+
reject(new Error("connect timeout"));
|
|
640
|
+
}, this.timeoutMs);
|
|
641
|
+
sock.once("connect", () => {
|
|
642
|
+
clearTimeout(to);
|
|
643
|
+
resolve(sock);
|
|
644
|
+
});
|
|
645
|
+
sock.once("error", (e) => {
|
|
646
|
+
clearTimeout(to);
|
|
647
|
+
reject(e);
|
|
648
|
+
});
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
trySpawnDaemon() {
|
|
652
|
+
let fd;
|
|
653
|
+
try {
|
|
654
|
+
fd = openSync(this.pidPath, "wx", 384);
|
|
655
|
+
writeSync(fd, String(process.pid));
|
|
656
|
+
} catch (e) {
|
|
657
|
+
if (this.isPidFileStale()) {
|
|
658
|
+
try {
|
|
659
|
+
unlinkSync(this.pidPath);
|
|
660
|
+
} catch {
|
|
661
|
+
}
|
|
662
|
+
try {
|
|
663
|
+
fd = openSync(this.pidPath, "wx", 384);
|
|
664
|
+
writeSync(fd, String(process.pid));
|
|
665
|
+
} catch {
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
} else {
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
if (!this.daemonEntry || !existsSync3(this.daemonEntry)) {
|
|
673
|
+
log3(`daemonEntry not configured or missing: ${this.daemonEntry}`);
|
|
674
|
+
try {
|
|
675
|
+
closeSync(fd);
|
|
676
|
+
unlinkSync(this.pidPath);
|
|
677
|
+
} catch {
|
|
678
|
+
}
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
try {
|
|
682
|
+
const child = spawn(process.execPath, [this.daemonEntry], {
|
|
683
|
+
detached: true,
|
|
684
|
+
stdio: "ignore",
|
|
685
|
+
env: process.env
|
|
686
|
+
});
|
|
687
|
+
child.unref();
|
|
688
|
+
log3(`spawned daemon pid=${child.pid}`);
|
|
689
|
+
} finally {
|
|
690
|
+
closeSync(fd);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
isPidFileStale() {
|
|
694
|
+
try {
|
|
695
|
+
const raw = readFileSync3(this.pidPath, "utf-8").trim();
|
|
696
|
+
const pid = Number(raw);
|
|
697
|
+
if (!pid || Number.isNaN(pid))
|
|
698
|
+
return true;
|
|
699
|
+
try {
|
|
700
|
+
process.kill(pid, 0);
|
|
701
|
+
return false;
|
|
702
|
+
} catch {
|
|
703
|
+
return true;
|
|
704
|
+
}
|
|
705
|
+
} catch {
|
|
706
|
+
return true;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
async waitForSocket() {
|
|
710
|
+
const deadline = Date.now() + this.spawnWaitMs;
|
|
711
|
+
let delay = 30;
|
|
712
|
+
while (Date.now() < deadline) {
|
|
713
|
+
await sleep2(delay);
|
|
714
|
+
delay = Math.min(delay * 1.5, 300);
|
|
715
|
+
if (!existsSync3(this.socketPath))
|
|
716
|
+
continue;
|
|
717
|
+
try {
|
|
718
|
+
return await this.connectOnce();
|
|
719
|
+
} catch {
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
throw new Error("daemon did not become ready within spawnWaitMs");
|
|
723
|
+
}
|
|
724
|
+
sendAndWait(sock, req) {
|
|
725
|
+
return new Promise((resolve, reject) => {
|
|
726
|
+
let buf = "";
|
|
727
|
+
const to = setTimeout(() => {
|
|
728
|
+
sock.destroy();
|
|
729
|
+
reject(new Error("request timeout"));
|
|
730
|
+
}, this.timeoutMs);
|
|
731
|
+
sock.setEncoding("utf-8");
|
|
732
|
+
sock.on("data", (chunk) => {
|
|
733
|
+
buf += chunk;
|
|
734
|
+
const nl = buf.indexOf("\n");
|
|
735
|
+
if (nl === -1)
|
|
736
|
+
return;
|
|
737
|
+
const line = buf.slice(0, nl);
|
|
738
|
+
clearTimeout(to);
|
|
739
|
+
try {
|
|
740
|
+
resolve(JSON.parse(line));
|
|
741
|
+
} catch (e) {
|
|
742
|
+
reject(e);
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
sock.on("error", (e) => {
|
|
746
|
+
clearTimeout(to);
|
|
747
|
+
reject(e);
|
|
748
|
+
});
|
|
749
|
+
sock.on("end", () => {
|
|
750
|
+
clearTimeout(to);
|
|
751
|
+
reject(new Error("connection closed without response"));
|
|
752
|
+
});
|
|
753
|
+
sock.write(JSON.stringify(req) + "\n");
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
function sleep2(ms) {
|
|
758
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// dist/src/embeddings/sql.js
|
|
762
|
+
function embeddingSqlLiteral(vec) {
|
|
763
|
+
if (!vec || vec.length === 0)
|
|
764
|
+
return "NULL";
|
|
765
|
+
const parts = [];
|
|
766
|
+
for (const v of vec) {
|
|
767
|
+
if (!Number.isFinite(v))
|
|
768
|
+
return "NULL";
|
|
769
|
+
parts.push(String(v));
|
|
770
|
+
}
|
|
771
|
+
return `ARRAY[${parts.join(",")}]::float4[]`;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// dist/src/embeddings/disable.js
|
|
775
|
+
import { createRequire } from "node:module";
|
|
776
|
+
import { homedir as homedir4 } from "node:os";
|
|
777
|
+
import { join as join5 } from "node:path";
|
|
778
|
+
import { pathToFileURL } from "node:url";
|
|
779
|
+
var cachedStatus = null;
|
|
780
|
+
function defaultResolveTransformers() {
|
|
781
|
+
try {
|
|
782
|
+
createRequire(import.meta.url).resolve("@huggingface/transformers");
|
|
783
|
+
return;
|
|
784
|
+
} catch {
|
|
785
|
+
}
|
|
786
|
+
const sharedDir = join5(homedir4(), ".hivemind", "embed-deps");
|
|
787
|
+
createRequire(pathToFileURL(`${sharedDir}/`).href).resolve("@huggingface/transformers");
|
|
788
|
+
}
|
|
789
|
+
var _resolve = defaultResolveTransformers;
|
|
790
|
+
function detectStatus() {
|
|
791
|
+
if (process.env.HIVEMIND_EMBEDDINGS === "false")
|
|
792
|
+
return "env-disabled";
|
|
793
|
+
try {
|
|
794
|
+
_resolve();
|
|
795
|
+
return "enabled";
|
|
796
|
+
} catch {
|
|
797
|
+
return "no-transformers";
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
function embeddingsStatus() {
|
|
801
|
+
if (cachedStatus !== null)
|
|
802
|
+
return cachedStatus;
|
|
803
|
+
cachedStatus = detectStatus();
|
|
804
|
+
return cachedStatus;
|
|
805
|
+
}
|
|
806
|
+
function embeddingsDisabled() {
|
|
807
|
+
return embeddingsStatus() !== "enabled";
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// dist/src/hooks/cursor/capture.js
|
|
811
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
812
|
+
import { dirname as dirname2, join as join9 } from "node:path";
|
|
813
|
+
|
|
814
|
+
// dist/src/hooks/summary-state.js
|
|
815
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, writeSync as writeSync2, mkdirSync as mkdirSync2, renameSync, existsSync as existsSync4, unlinkSync as unlinkSync2, openSync as openSync2, closeSync as closeSync2 } from "node:fs";
|
|
816
|
+
import { homedir as homedir5 } from "node:os";
|
|
817
|
+
import { join as join6 } from "node:path";
|
|
818
|
+
var dlog = (msg) => log("summary-state", msg);
|
|
819
|
+
var STATE_DIR = join6(homedir5(), ".claude", "hooks", "summary-state");
|
|
820
|
+
var YIELD_BUF = new Int32Array(new SharedArrayBuffer(4));
|
|
821
|
+
function statePath(sessionId) {
|
|
822
|
+
return join6(STATE_DIR, `${sessionId}.json`);
|
|
823
|
+
}
|
|
824
|
+
function lockPath(sessionId) {
|
|
825
|
+
return join6(STATE_DIR, `${sessionId}.lock`);
|
|
826
|
+
}
|
|
827
|
+
function readState(sessionId) {
|
|
828
|
+
const p = statePath(sessionId);
|
|
829
|
+
if (!existsSync4(p))
|
|
830
|
+
return null;
|
|
831
|
+
try {
|
|
832
|
+
return JSON.parse(readFileSync4(p, "utf-8"));
|
|
833
|
+
} catch {
|
|
834
|
+
return null;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
function writeState(sessionId, state) {
|
|
838
|
+
mkdirSync2(STATE_DIR, { recursive: true });
|
|
839
|
+
const p = statePath(sessionId);
|
|
840
|
+
const tmp = `${p}.${process.pid}.${Date.now()}.tmp`;
|
|
841
|
+
writeFileSync2(tmp, JSON.stringify(state));
|
|
842
|
+
renameSync(tmp, p);
|
|
843
|
+
}
|
|
844
|
+
function withRmwLock(sessionId, fn) {
|
|
845
|
+
mkdirSync2(STATE_DIR, { recursive: true });
|
|
846
|
+
const rmwLock = statePath(sessionId) + ".rmw";
|
|
847
|
+
const deadline = Date.now() + 2e3;
|
|
848
|
+
let fd = null;
|
|
849
|
+
while (fd === null) {
|
|
850
|
+
try {
|
|
851
|
+
fd = openSync2(rmwLock, "wx");
|
|
852
|
+
} catch (e) {
|
|
853
|
+
if (e.code !== "EEXIST")
|
|
854
|
+
throw e;
|
|
855
|
+
if (Date.now() > deadline) {
|
|
856
|
+
dlog(`rmw lock deadline exceeded for ${sessionId}, reclaiming stale lock`);
|
|
857
|
+
try {
|
|
858
|
+
unlinkSync2(rmwLock);
|
|
859
|
+
} catch (unlinkErr) {
|
|
860
|
+
dlog(`stale rmw lock unlink failed for ${sessionId}: ${unlinkErr.message}`);
|
|
861
|
+
}
|
|
862
|
+
continue;
|
|
863
|
+
}
|
|
864
|
+
Atomics.wait(YIELD_BUF, 0, 0, 10);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
try {
|
|
868
|
+
return fn();
|
|
869
|
+
} finally {
|
|
870
|
+
closeSync2(fd);
|
|
871
|
+
try {
|
|
872
|
+
unlinkSync2(rmwLock);
|
|
873
|
+
} catch (unlinkErr) {
|
|
874
|
+
dlog(`rmw lock cleanup failed for ${sessionId}: ${unlinkErr.message}`);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
function bumpTotalCount(sessionId) {
|
|
879
|
+
return withRmwLock(sessionId, () => {
|
|
880
|
+
const now = Date.now();
|
|
881
|
+
const existing = readState(sessionId);
|
|
882
|
+
const next = existing ? { ...existing, totalCount: existing.totalCount + 1 } : { lastSummaryAt: now, lastSummaryCount: 0, totalCount: 1 };
|
|
883
|
+
writeState(sessionId, next);
|
|
884
|
+
return next;
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
function loadTriggerConfig() {
|
|
888
|
+
const n = Number(process.env.HIVEMIND_SUMMARY_EVERY_N_MSGS ?? "");
|
|
889
|
+
const h = Number(process.env.HIVEMIND_SUMMARY_EVERY_HOURS ?? "");
|
|
890
|
+
return {
|
|
891
|
+
everyNMessages: Number.isInteger(n) && n > 0 ? n : 50,
|
|
892
|
+
everyHours: Number.isFinite(h) && h > 0 ? h : 2
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
var FIRST_SUMMARY_AT = 10;
|
|
896
|
+
function shouldTrigger(state, cfg, now = Date.now()) {
|
|
897
|
+
const msgsSince = state.totalCount - state.lastSummaryCount;
|
|
898
|
+
if (state.lastSummaryCount === 0 && state.totalCount >= FIRST_SUMMARY_AT)
|
|
899
|
+
return true;
|
|
900
|
+
if (msgsSince >= cfg.everyNMessages)
|
|
901
|
+
return true;
|
|
902
|
+
if (msgsSince > 0 && now - state.lastSummaryAt >= cfg.everyHours * 3600 * 1e3)
|
|
903
|
+
return true;
|
|
904
|
+
return false;
|
|
905
|
+
}
|
|
906
|
+
function tryAcquireLock(sessionId, maxAgeMs = 10 * 60 * 1e3) {
|
|
907
|
+
mkdirSync2(STATE_DIR, { recursive: true });
|
|
908
|
+
const p = lockPath(sessionId);
|
|
909
|
+
if (existsSync4(p)) {
|
|
910
|
+
try {
|
|
911
|
+
const ageMs = Date.now() - parseInt(readFileSync4(p, "utf-8"), 10);
|
|
912
|
+
if (Number.isFinite(ageMs) && ageMs < maxAgeMs)
|
|
913
|
+
return false;
|
|
914
|
+
} catch (readErr) {
|
|
915
|
+
dlog(`lock file unreadable for ${sessionId}, treating as stale: ${readErr.message}`);
|
|
916
|
+
}
|
|
917
|
+
try {
|
|
918
|
+
unlinkSync2(p);
|
|
919
|
+
} catch (unlinkErr) {
|
|
920
|
+
dlog(`could not unlink stale lock for ${sessionId}: ${unlinkErr.message}`);
|
|
921
|
+
return false;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
try {
|
|
925
|
+
const fd = openSync2(p, "wx");
|
|
926
|
+
try {
|
|
927
|
+
writeSync2(fd, String(Date.now()));
|
|
928
|
+
} finally {
|
|
929
|
+
closeSync2(fd);
|
|
930
|
+
}
|
|
931
|
+
return true;
|
|
932
|
+
} catch (e) {
|
|
933
|
+
if (e.code === "EEXIST")
|
|
934
|
+
return false;
|
|
935
|
+
throw e;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
function releaseLock(sessionId) {
|
|
939
|
+
try {
|
|
940
|
+
unlinkSync2(lockPath(sessionId));
|
|
941
|
+
} catch (e) {
|
|
942
|
+
if (e?.code !== "ENOENT") {
|
|
943
|
+
dlog(`releaseLock unlink failed for ${sessionId}: ${e.message}`);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// dist/src/hooks/cursor/spawn-wiki-worker.js
|
|
949
|
+
import { spawn as spawn2, execSync } from "node:child_process";
|
|
950
|
+
import { fileURLToPath } from "node:url";
|
|
951
|
+
import { dirname, join as join8 } from "node:path";
|
|
952
|
+
import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "node:fs";
|
|
953
|
+
import { homedir as homedir6, tmpdir as tmpdir2 } from "node:os";
|
|
954
|
+
|
|
955
|
+
// dist/src/utils/wiki-log.js
|
|
956
|
+
import { mkdirSync as mkdirSync3, appendFileSync as appendFileSync2 } from "node:fs";
|
|
957
|
+
import { join as join7 } from "node:path";
|
|
958
|
+
function makeWikiLogger(hooksDir, filename = "deeplake-wiki.log") {
|
|
959
|
+
const path = join7(hooksDir, filename);
|
|
960
|
+
return {
|
|
961
|
+
path,
|
|
962
|
+
log(msg) {
|
|
963
|
+
try {
|
|
964
|
+
mkdirSync3(hooksDir, { recursive: true });
|
|
965
|
+
appendFileSync2(path, `[${utcTimestamp()}] ${msg}
|
|
966
|
+
`);
|
|
967
|
+
} catch {
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// dist/src/hooks/cursor/spawn-wiki-worker.js
|
|
974
|
+
var HOME = homedir6();
|
|
975
|
+
var wikiLogger = makeWikiLogger(join8(HOME, ".cursor", "hooks"));
|
|
976
|
+
var WIKI_LOG = wikiLogger.path;
|
|
977
|
+
var WIKI_PROMPT_TEMPLATE = `You are building a personal wiki from a coding session. Your goal is to extract every piece of knowledge \u2014 entities, decisions, relationships, and facts \u2014 into a structured, searchable wiki entry.
|
|
978
|
+
|
|
979
|
+
SESSION JSONL path: __JSONL__
|
|
980
|
+
SUMMARY FILE to write: __SUMMARY__
|
|
981
|
+
SESSION ID: __SESSION_ID__
|
|
982
|
+
PROJECT: __PROJECT__
|
|
983
|
+
PREVIOUS JSONL OFFSET (lines already processed): __PREV_OFFSET__
|
|
984
|
+
CURRENT JSONL LINES: __JSONL_LINES__
|
|
985
|
+
|
|
986
|
+
Steps:
|
|
987
|
+
1. Read the session JSONL at the path above.
|
|
988
|
+
- If PREVIOUS JSONL OFFSET > 0, this is a resumed session. Read the existing summary file first,
|
|
989
|
+
then focus on lines AFTER the offset for new content. Merge new facts into the existing summary.
|
|
990
|
+
- If offset is 0, generate from scratch.
|
|
991
|
+
|
|
992
|
+
2. Write the summary file at the path above with this EXACT format:
|
|
993
|
+
|
|
994
|
+
# Session __SESSION_ID__
|
|
995
|
+
- **Source**: __JSONL_SERVER_PATH__
|
|
996
|
+
- **Started**: <extract from JSONL>
|
|
997
|
+
- **Ended**: <now>
|
|
998
|
+
- **Project**: __PROJECT__
|
|
999
|
+
- **JSONL offset**: __JSONL_LINES__
|
|
1000
|
+
|
|
1001
|
+
## What Happened
|
|
1002
|
+
<2-3 dense sentences. What was the goal, what was accomplished, what's left.>
|
|
1003
|
+
|
|
1004
|
+
## People
|
|
1005
|
+
<For each person mentioned: name, role, what they did/said. Format: **Name** \u2014 role \u2014 action>
|
|
1006
|
+
|
|
1007
|
+
## Entities
|
|
1008
|
+
<Every named thing: repos, branches, files, APIs, tools, services, tables, features, bugs.
|
|
1009
|
+
Format: **entity** (type) \u2014 what was done with it, its current state>
|
|
1010
|
+
|
|
1011
|
+
## Decisions & Reasoning
|
|
1012
|
+
<Every decision made and WHY.>
|
|
1013
|
+
|
|
1014
|
+
## Key Facts
|
|
1015
|
+
<Bullet list of atomic facts that could answer future questions.>
|
|
1016
|
+
|
|
1017
|
+
## Files Modified
|
|
1018
|
+
<bullet list: path (new/modified/deleted) \u2014 what changed>
|
|
1019
|
+
|
|
1020
|
+
## Open Questions / TODO
|
|
1021
|
+
<Anything unresolved, blocked, or explicitly deferred>
|
|
1022
|
+
|
|
1023
|
+
IMPORTANT: Be exhaustive. Extract EVERY entity, decision, and fact.
|
|
1024
|
+
PRIVACY: Never include absolute filesystem paths in the summary.
|
|
1025
|
+
LENGTH LIMIT: Keep the total summary under 4000 characters.`;
|
|
1026
|
+
var wikiLog = wikiLogger.log;
|
|
1027
|
+
function findCursorBin() {
|
|
1028
|
+
try {
|
|
1029
|
+
return execSync("which cursor-agent 2>/dev/null", { encoding: "utf-8" }).trim() || "cursor-agent";
|
|
1030
|
+
} catch {
|
|
1031
|
+
return "cursor-agent";
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
function spawnCursorWikiWorker(opts) {
|
|
1035
|
+
const { config, sessionId, cwd, bundleDir, reason } = opts;
|
|
1036
|
+
const projectName = cwd.split("/").pop() || "unknown";
|
|
1037
|
+
const tmpDir = join8(tmpdir2(), `deeplake-wiki-${sessionId}-${Date.now()}`);
|
|
1038
|
+
mkdirSync4(tmpDir, { recursive: true });
|
|
1039
|
+
const configFile = join8(tmpDir, "config.json");
|
|
1040
|
+
writeFileSync3(configFile, JSON.stringify({
|
|
1041
|
+
apiUrl: config.apiUrl,
|
|
1042
|
+
token: config.token,
|
|
1043
|
+
orgId: config.orgId,
|
|
1044
|
+
workspaceId: config.workspaceId,
|
|
1045
|
+
memoryTable: config.tableName,
|
|
1046
|
+
sessionsTable: config.sessionsTableName,
|
|
1047
|
+
sessionId,
|
|
1048
|
+
userName: config.userName,
|
|
1049
|
+
project: projectName,
|
|
1050
|
+
tmpDir,
|
|
1051
|
+
cursorBin: findCursorBin(),
|
|
1052
|
+
cursorModel: process.env.HIVEMIND_CURSOR_MODEL ?? "auto",
|
|
1053
|
+
wikiLog: WIKI_LOG,
|
|
1054
|
+
hooksDir: join8(HOME, ".cursor", "hooks"),
|
|
1055
|
+
promptTemplate: WIKI_PROMPT_TEMPLATE
|
|
1056
|
+
}));
|
|
1057
|
+
wikiLog(`${reason}: spawning summary worker for ${sessionId}`);
|
|
1058
|
+
const workerPath = join8(bundleDir, "wiki-worker.js");
|
|
1059
|
+
spawn2("nohup", ["node", workerPath, configFile], {
|
|
1060
|
+
detached: true,
|
|
1061
|
+
stdio: ["ignore", "ignore", "ignore"]
|
|
1062
|
+
}).unref();
|
|
1063
|
+
wikiLog(`${reason}: spawned summary worker for ${sessionId}`);
|
|
1064
|
+
}
|
|
1065
|
+
function bundleDirFromImportMeta(importMetaUrl) {
|
|
1066
|
+
return dirname(fileURLToPath(importMetaUrl));
|
|
1067
|
+
}
|
|
1068
|
+
|
|
396
1069
|
// dist/src/hooks/cursor/capture.js
|
|
397
|
-
var
|
|
1070
|
+
var log4 = (msg) => log("cursor-capture", msg);
|
|
1071
|
+
function resolveEmbedDaemonPath() {
|
|
1072
|
+
return join9(dirname2(fileURLToPath2(import.meta.url)), "embeddings", "embed-daemon.js");
|
|
1073
|
+
}
|
|
398
1074
|
var CAPTURE = process.env.HIVEMIND_CAPTURE !== "false";
|
|
399
1075
|
function resolveCwd(input) {
|
|
400
1076
|
if (typeof input.cwd === "string" && input.cwd)
|
|
@@ -410,7 +1086,7 @@ async function main() {
|
|
|
410
1086
|
const input = await readStdin();
|
|
411
1087
|
const config = loadConfig();
|
|
412
1088
|
if (!config) {
|
|
413
|
-
|
|
1089
|
+
log4("no config");
|
|
414
1090
|
return;
|
|
415
1091
|
}
|
|
416
1092
|
const sessionId = input.conversation_id ?? `cursor-${Date.now()}`;
|
|
@@ -429,10 +1105,10 @@ async function main() {
|
|
|
429
1105
|
};
|
|
430
1106
|
let entry = null;
|
|
431
1107
|
if (event === "beforeSubmitPrompt" && typeof input.prompt === "string") {
|
|
432
|
-
|
|
1108
|
+
log4(`user session=${sessionId}`);
|
|
433
1109
|
entry = { id: crypto.randomUUID(), ...meta, type: "user_message", content: input.prompt };
|
|
434
1110
|
} else if (event === "postToolUse" && typeof input.tool_name === "string") {
|
|
435
|
-
|
|
1111
|
+
log4(`tool=${input.tool_name} session=${sessionId}`);
|
|
436
1112
|
entry = {
|
|
437
1113
|
id: crypto.randomUUID(),
|
|
438
1114
|
...meta,
|
|
@@ -444,10 +1120,10 @@ async function main() {
|
|
|
444
1120
|
tool_response: typeof input.tool_output === "string" ? input.tool_output : JSON.stringify(input.tool_output)
|
|
445
1121
|
};
|
|
446
1122
|
} else if (event === "afterAgentResponse" && typeof input.text === "string") {
|
|
447
|
-
|
|
1123
|
+
log4(`assistant session=${sessionId}`);
|
|
448
1124
|
entry = { id: crypto.randomUUID(), ...meta, type: "assistant_message", content: input.text };
|
|
449
1125
|
} else if (event === "stop") {
|
|
450
|
-
|
|
1126
|
+
log4(`stop session=${sessionId} status=${input.status ?? "unknown"}`);
|
|
451
1127
|
entry = {
|
|
452
1128
|
id: crypto.randomUUID(),
|
|
453
1129
|
...meta,
|
|
@@ -456,30 +1132,65 @@ async function main() {
|
|
|
456
1132
|
loop_count: input.loop_count
|
|
457
1133
|
};
|
|
458
1134
|
} else {
|
|
459
|
-
|
|
1135
|
+
log4(`unknown event: ${event}, skipping`);
|
|
460
1136
|
return;
|
|
461
1137
|
}
|
|
462
1138
|
const sessionPath = buildSessionPath(config, sessionId);
|
|
463
1139
|
const line = JSON.stringify(entry);
|
|
464
|
-
|
|
1140
|
+
log4(`writing to ${sessionPath}`);
|
|
465
1141
|
const projectName = cwd.split("/").pop() || "unknown";
|
|
466
1142
|
const filename = sessionPath.split("/").pop() ?? "";
|
|
467
1143
|
const jsonForSql = line.replace(/'/g, "''");
|
|
468
|
-
const
|
|
1144
|
+
const embedding = embeddingsDisabled() ? null : await new EmbedClient({ daemonEntry: resolveEmbedDaemonPath() }).embed(line, "document");
|
|
1145
|
+
const embeddingSql = embeddingSqlLiteral(embedding);
|
|
1146
|
+
const insertSql = `INSERT INTO "${sessionsTable}" (id, path, filename, message, message_embedding, author, size_bytes, project, description, agent, creation_date, last_update_date) VALUES ('${crypto.randomUUID()}', '${sqlStr(sessionPath)}', '${sqlStr(filename)}', '${jsonForSql}'::jsonb, ${embeddingSql}, '${sqlStr(config.userName)}', ${Buffer.byteLength(line, "utf-8")}, '${sqlStr(projectName)}', '${sqlStr(event)}', 'cursor', '${ts}', '${ts}')`;
|
|
469
1147
|
try {
|
|
470
1148
|
await api.query(insertSql);
|
|
471
1149
|
} catch (e) {
|
|
472
1150
|
if (e.message?.includes("permission denied") || e.message?.includes("does not exist")) {
|
|
473
|
-
|
|
1151
|
+
log4("table missing, creating and retrying");
|
|
474
1152
|
await api.ensureSessionsTable(sessionsTable);
|
|
475
1153
|
await api.query(insertSql);
|
|
476
1154
|
} else {
|
|
477
1155
|
throw e;
|
|
478
1156
|
}
|
|
479
1157
|
}
|
|
480
|
-
|
|
1158
|
+
log4("capture ok \u2192 cloud");
|
|
1159
|
+
maybeTriggerPeriodicSummary(sessionId, cwd, config);
|
|
1160
|
+
}
|
|
1161
|
+
function maybeTriggerPeriodicSummary(sessionId, cwd, config) {
|
|
1162
|
+
if (process.env.HIVEMIND_WIKI_WORKER === "1")
|
|
1163
|
+
return;
|
|
1164
|
+
try {
|
|
1165
|
+
const state = bumpTotalCount(sessionId);
|
|
1166
|
+
const cfg = loadTriggerConfig();
|
|
1167
|
+
if (!shouldTrigger(state, cfg))
|
|
1168
|
+
return;
|
|
1169
|
+
if (!tryAcquireLock(sessionId)) {
|
|
1170
|
+
log4(`periodic trigger suppressed (lock held) session=${sessionId}`);
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
wikiLog(`Periodic: threshold hit (total=${state.totalCount}, since=${state.totalCount - state.lastSummaryCount}, N=${cfg.everyNMessages}, hours=${cfg.everyHours})`);
|
|
1174
|
+
try {
|
|
1175
|
+
spawnCursorWikiWorker({
|
|
1176
|
+
config,
|
|
1177
|
+
sessionId,
|
|
1178
|
+
cwd,
|
|
1179
|
+
bundleDir: bundleDirFromImportMeta(import.meta.url),
|
|
1180
|
+
reason: "Periodic"
|
|
1181
|
+
});
|
|
1182
|
+
} catch (e) {
|
|
1183
|
+
log4(`periodic spawn failed: ${e.message}`);
|
|
1184
|
+
try {
|
|
1185
|
+
releaseLock(sessionId);
|
|
1186
|
+
} catch {
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
} catch (e) {
|
|
1190
|
+
log4(`periodic trigger error: ${e.message}`);
|
|
1191
|
+
}
|
|
481
1192
|
}
|
|
482
1193
|
main().catch((e) => {
|
|
483
|
-
|
|
1194
|
+
log4(`fatal: ${e.message}`);
|
|
484
1195
|
process.exit(0);
|
|
485
1196
|
});
|