@deeplake/hivemind 0.6.47 → 0.6.48
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 +11 -31
- package/bundle/cli.js +3597 -233
- package/codex/bundle/capture.js +1 -1
- package/codex/bundle/commands/auth-login.js +10 -6
- package/codex/bundle/pre-tool-use.js +84 -4
- package/codex/bundle/session-start-setup.js +15 -1
- package/codex/bundle/session-start.js +15 -1
- package/codex/bundle/stop.js +1 -1
- package/cursor/bundle/commands/auth-login.js +10 -6
- package/cursor/bundle/pre-tool-use.js +1193 -0
- package/cursor/bundle/session-start.js +17 -3
- package/hermes/bundle/capture.js +481 -0
- package/hermes/bundle/commands/auth-login.js +866 -0
- package/hermes/bundle/package.json +1 -0
- package/hermes/bundle/pre-tool-use.js +1190 -0
- package/hermes/bundle/session-end.js +42 -0
- package/hermes/bundle/session-start.js +514 -0
- package/hermes/bundle/shell/deeplake-shell.js +69338 -0
- package/mcp/bundle/server.js +37 -2
- package/openclaw/dist/index.js +1 -1
- package/openclaw/openclaw.plugin.json +1 -1
- package/openclaw/package.json +1 -1
- package/package.json +6 -3
- package/pi/extension-source/hivemind.ts +355 -0
package/mcp/bundle/server.js
CHANGED
|
@@ -23943,6 +23943,41 @@ function buildContentFilter(column, likeOp, patterns) {
|
|
|
23943
23943
|
return ` AND (${patterns.map((pattern) => `${column} ${likeOp} '%${pattern}%'`).join(" OR ")})`;
|
|
23944
23944
|
}
|
|
23945
23945
|
|
|
23946
|
+
// dist/src/cli/version.js
|
|
23947
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
23948
|
+
import { join as join6 } from "node:path";
|
|
23949
|
+
|
|
23950
|
+
// dist/src/cli/util.js
|
|
23951
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3, cpSync, symlinkSync, unlinkSync as unlinkSync2, lstatSync } from "node:fs";
|
|
23952
|
+
import { join as join5, dirname } from "node:path";
|
|
23953
|
+
import { homedir as homedir4 } from "node:os";
|
|
23954
|
+
import { fileURLToPath } from "node:url";
|
|
23955
|
+
var HOME = homedir4();
|
|
23956
|
+
function pkgRoot() {
|
|
23957
|
+
return fileURLToPath(new URL("..", import.meta.url));
|
|
23958
|
+
}
|
|
23959
|
+
var PLATFORM_MARKERS = [
|
|
23960
|
+
{ id: "claude", markerDir: join5(HOME, ".claude") },
|
|
23961
|
+
{ id: "codex", markerDir: join5(HOME, ".codex") },
|
|
23962
|
+
{ id: "claw", markerDir: join5(HOME, ".openclaw") },
|
|
23963
|
+
{ id: "cursor", markerDir: join5(HOME, ".cursor") },
|
|
23964
|
+
{ id: "hermes", markerDir: join5(HOME, ".hermes") },
|
|
23965
|
+
// pi (badlogic/pi-mono coding-agent) — config at ~/.pi/agent/. pi exposes
|
|
23966
|
+
// a rich extension event API (session_start / input / tool_call /
|
|
23967
|
+
// tool_result / message_end / session_shutdown / etc.) — Tier 1 capable.
|
|
23968
|
+
{ id: "pi", markerDir: join5(HOME, ".pi") }
|
|
23969
|
+
];
|
|
23970
|
+
|
|
23971
|
+
// dist/src/cli/version.js
|
|
23972
|
+
function getVersion() {
|
|
23973
|
+
try {
|
|
23974
|
+
const pkg = JSON.parse(readFileSync5(join6(pkgRoot(), "package.json"), "utf-8"));
|
|
23975
|
+
return pkg.version ?? "0.0.0";
|
|
23976
|
+
} catch {
|
|
23977
|
+
return "0.0.0";
|
|
23978
|
+
}
|
|
23979
|
+
}
|
|
23980
|
+
|
|
23946
23981
|
// dist/src/mcp/server.js
|
|
23947
23982
|
function getContext() {
|
|
23948
23983
|
const creds = loadCredentials();
|
|
@@ -23961,7 +23996,7 @@ function errorResult(text) {
|
|
|
23961
23996
|
}
|
|
23962
23997
|
var server = new McpServer({
|
|
23963
23998
|
name: "hivemind",
|
|
23964
|
-
version:
|
|
23999
|
+
version: getVersion()
|
|
23965
24000
|
});
|
|
23966
24001
|
server.registerTool("hivemind_search", {
|
|
23967
24002
|
description: "Search Hivemind shared memory (summaries + raw sessions) by keyword or multi-word phrase. Returns matching paths and snippets. Use this first when the user asks about prior work, conversations, or context that may exist in Hivemind. Different paths under /summaries/<username>/ are different users \u2014 do not merge them.",
|
|
@@ -24037,7 +24072,7 @@ server.registerTool("hivemind_index", {
|
|
|
24037
24072
|
const ctx = getContext();
|
|
24038
24073
|
if ("error" in ctx)
|
|
24039
24074
|
return errorResult(ctx.error);
|
|
24040
|
-
const where = prefix ? `WHERE path LIKE '${
|
|
24075
|
+
const where = prefix ? `WHERE path LIKE '${sqlLike(prefix)}%' ESCAPE '\\'` : `WHERE path LIKE '/summaries/%'`;
|
|
24041
24076
|
const sql = `SELECT path, description, project, last_update_date FROM "${ctx.memoryTable}" ${where} ORDER BY last_update_date DESC LIMIT ${limit ?? 50}`;
|
|
24042
24077
|
try {
|
|
24043
24078
|
const rows = await ctx.api.query(sql);
|
package/openclaw/dist/index.js
CHANGED
|
@@ -984,7 +984,7 @@ function extractLatestVersion(body) {
|
|
|
984
984
|
return typeof v === "string" && v.length > 0 ? v : null;
|
|
985
985
|
}
|
|
986
986
|
function getInstalledVersion() {
|
|
987
|
-
return "0.6.
|
|
987
|
+
return "0.6.48".length > 0 ? "0.6.48" : null;
|
|
988
988
|
}
|
|
989
989
|
function isNewer(latest, current) {
|
|
990
990
|
const parse = (v) => v.replace(/-.*$/, "").split(".").map(Number);
|
package/openclaw/package.json
CHANGED
package/package.json
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deeplake/hivemind",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.48",
|
|
4
4
|
"description": "Cloud-backed persistent shared memory for AI agents powered by Deeplake",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
8
8
|
},
|
|
9
9
|
"bin": {
|
|
10
|
-
"hivemind": "bundle/cli.js"
|
|
11
|
-
"deeplake-shell": "bundle/deeplake-shell.js"
|
|
10
|
+
"hivemind": "bundle/cli.js"
|
|
12
11
|
},
|
|
13
12
|
"files": [
|
|
14
13
|
"bundle",
|
|
15
14
|
"codex/bundle",
|
|
16
15
|
"codex/skills",
|
|
17
16
|
"cursor/bundle",
|
|
17
|
+
"hermes/bundle",
|
|
18
18
|
"mcp/bundle",
|
|
19
|
+
"pi/extension-source",
|
|
19
20
|
"openclaw/dist",
|
|
20
21
|
"openclaw/skills",
|
|
21
22
|
"openclaw/openclaw.plugin.json",
|
|
@@ -47,11 +48,13 @@
|
|
|
47
48
|
"dependencies": {
|
|
48
49
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
49
50
|
"deeplake": "^0.3.30",
|
|
51
|
+
"js-yaml": "^4.1.1",
|
|
50
52
|
"just-bash": "^2.14.0",
|
|
51
53
|
"yargs-parser": "^22.0.0",
|
|
52
54
|
"zod": "^4.3.6"
|
|
53
55
|
},
|
|
54
56
|
"devDependencies": {
|
|
57
|
+
"@types/js-yaml": "^4.0.9",
|
|
55
58
|
"@types/node": "^25.0.0",
|
|
56
59
|
"@types/yargs-parser": "^21.0.3",
|
|
57
60
|
"@vitest/coverage-v8": "^4.1.3",
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
// @ts-nocheck — distributed as raw .ts; pi's runtime loads + compiles it.
|
|
2
|
+
// We ship this file verbatim into ~/.pi/agent/extensions/hivemind.ts.
|
|
3
|
+
//
|
|
4
|
+
// Hivemind extension for pi (badlogic/pi-mono coding-agent).
|
|
5
|
+
//
|
|
6
|
+
// Subscribes to the agent lifecycle events documented in
|
|
7
|
+
// `pi-mono/packages/coding-agent/src/core/extensions/types.ts` to:
|
|
8
|
+
// - inject deeplake memory context at session_start
|
|
9
|
+
// - capture user prompts (input event)
|
|
10
|
+
// - capture tool call results (tool_result event)
|
|
11
|
+
// - capture assistant messages (message_end event)
|
|
12
|
+
// - finalize on session_shutdown
|
|
13
|
+
//
|
|
14
|
+
// Plus registers three first-class pi tools (since pi has no MCP):
|
|
15
|
+
// - hivemind_search
|
|
16
|
+
// - hivemind_read
|
|
17
|
+
// - hivemind_index
|
|
18
|
+
//
|
|
19
|
+
// All deeplake interactions are inline `fetch` calls so this file has
|
|
20
|
+
// zero non-builtin runtime dependencies — it only needs Node 22+ globals.
|
|
21
|
+
//
|
|
22
|
+
// Type imports are erased at runtime so they don't need to be installed
|
|
23
|
+
// at our build time. pi's `@mariozechner/pi-coding-agent` types are
|
|
24
|
+
// available to pi's compiler when this is loaded.
|
|
25
|
+
|
|
26
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
27
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
28
|
+
import { homedir } from "node:os";
|
|
29
|
+
import { join } from "node:path";
|
|
30
|
+
|
|
31
|
+
// ---------- credentials / config -----------------------------------------------
|
|
32
|
+
|
|
33
|
+
interface Creds {
|
|
34
|
+
token: string;
|
|
35
|
+
apiUrl: string;
|
|
36
|
+
orgId: string;
|
|
37
|
+
orgName?: string;
|
|
38
|
+
workspaceId: string;
|
|
39
|
+
userName: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function loadCreds(): Creds | null {
|
|
43
|
+
const path = join(homedir(), ".deeplake", "credentials.json");
|
|
44
|
+
if (!existsSync(path)) return null;
|
|
45
|
+
try {
|
|
46
|
+
const raw = readFileSync(path, "utf-8");
|
|
47
|
+
const parsed = JSON.parse(raw);
|
|
48
|
+
if (!parsed?.token) return null;
|
|
49
|
+
return {
|
|
50
|
+
token: parsed.token,
|
|
51
|
+
apiUrl: parsed.apiUrl ?? "https://api.deeplake.ai",
|
|
52
|
+
orgId: parsed.orgId,
|
|
53
|
+
orgName: parsed.orgName,
|
|
54
|
+
workspaceId: parsed.workspaceId ?? "default",
|
|
55
|
+
userName: parsed.userName ?? "unknown",
|
|
56
|
+
};
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const MEMORY_TABLE = process.env.HIVEMIND_TABLE ?? "memory";
|
|
63
|
+
const SESSIONS_TABLE = process.env.HIVEMIND_SESSIONS_TABLE ?? "sessions";
|
|
64
|
+
|
|
65
|
+
// ---------- SQL escape (matches src/utils/sql.ts) ------------------------------
|
|
66
|
+
|
|
67
|
+
function sqlStr(value: string): string {
|
|
68
|
+
return value
|
|
69
|
+
.replace(/\\/g, "\\\\")
|
|
70
|
+
.replace(/'/g, "''")
|
|
71
|
+
.replace(/\0/g, "")
|
|
72
|
+
.replace(/[\x01-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// LIKE-pattern escape: sqlStr only handles SQL string quoting, NOT LIKE
|
|
76
|
+
// metacharacters. Without this, a tool arg containing `%` or `_` (which
|
|
77
|
+
// the LLM controls via the tool schema) would bypass the intended path
|
|
78
|
+
// filter — e.g. prefix='%' would match every row in the table. Wrap the
|
|
79
|
+
// resulting LIKE clause with `ESCAPE '\\'` so the engine honours the
|
|
80
|
+
// backslash escaping below.
|
|
81
|
+
function sqlLike(value: string): string {
|
|
82
|
+
return sqlStr(value)
|
|
83
|
+
.replace(/\\/g, "\\\\")
|
|
84
|
+
.replace(/%/g, "\\%")
|
|
85
|
+
.replace(/_/g, "\\_");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// JSONB column escape — only single-quote doubling, preserves JSON escape sequences.
|
|
89
|
+
function sqlJsonb(json: string): string {
|
|
90
|
+
return json.replace(/'/g, "''");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---------- deeplake api -------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
async function dlQuery(creds: Creds, sql: string): Promise<unknown[]> {
|
|
96
|
+
const resp = await fetch(`${creds.apiUrl}/workspaces/${creds.workspaceId}/tables/query`, {
|
|
97
|
+
method: "POST",
|
|
98
|
+
headers: {
|
|
99
|
+
"Authorization": `Bearer ${creds.token}`,
|
|
100
|
+
"Content-Type": "application/json",
|
|
101
|
+
"X-Activeloop-Org-Id": creds.orgId,
|
|
102
|
+
},
|
|
103
|
+
body: JSON.stringify({ query: sql }),
|
|
104
|
+
});
|
|
105
|
+
if (!resp.ok) {
|
|
106
|
+
const text = await resp.text().catch(() => "");
|
|
107
|
+
throw new Error(`deeplake query failed: ${resp.status} ${text.slice(0, 200)}`);
|
|
108
|
+
}
|
|
109
|
+
const json = (await resp.json()) as { columns?: string[]; rows?: unknown[][] };
|
|
110
|
+
if (!json.rows || !json.columns) return [];
|
|
111
|
+
return json.rows.map((r) => Object.fromEntries(json.columns!.map((c, i) => [c, r[i]])));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---------- session-row writer -------------------------------------------------
|
|
115
|
+
|
|
116
|
+
function buildSessionPath(creds: Creds, sessionId: string): string {
|
|
117
|
+
const filename = `${creds.userName}_${creds.orgName ?? creds.orgId}_${creds.workspaceId}_${sessionId}.jsonl`;
|
|
118
|
+
return `/sessions/${creds.userName}/${filename}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function writeSessionRow(
|
|
122
|
+
creds: Creds,
|
|
123
|
+
sessionId: string,
|
|
124
|
+
agent: string,
|
|
125
|
+
event: string,
|
|
126
|
+
cwd: string,
|
|
127
|
+
entry: Record<string, unknown>,
|
|
128
|
+
): Promise<void> {
|
|
129
|
+
const ts = new Date().toISOString();
|
|
130
|
+
const sessionPath = buildSessionPath(creds, sessionId);
|
|
131
|
+
const filename = sessionPath.split("/").pop() ?? "";
|
|
132
|
+
const projectName = (cwd ?? "").split("/").pop() || "unknown";
|
|
133
|
+
const line = JSON.stringify(entry);
|
|
134
|
+
const jsonForSql = sqlJsonb(line);
|
|
135
|
+
const insertSql =
|
|
136
|
+
`INSERT INTO "${SESSIONS_TABLE}" (id, path, filename, message, author, size_bytes, project, description, agent, creation_date, last_update_date) ` +
|
|
137
|
+
`VALUES ('${crypto.randomUUID()}', '${sqlStr(sessionPath)}', '${sqlStr(filename)}', '${jsonForSql}'::jsonb, '${sqlStr(creds.userName)}', ` +
|
|
138
|
+
`${Buffer.byteLength(line, "utf-8")}, '${sqlStr(projectName)}', '${sqlStr(event)}', '${agent}', '${ts}', '${ts}')`;
|
|
139
|
+
await dlQuery(creds, insertSql);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ---------- search primitive (used by hivemind_search) -------------------------
|
|
143
|
+
|
|
144
|
+
async function searchTables(creds: Creds, query: string, limit: number): Promise<string> {
|
|
145
|
+
// ILIKE pattern: escape both SQL quotes AND LIKE wildcards. ESCAPE '\\'
|
|
146
|
+
// tells the engine to treat backslash as the escape character so our
|
|
147
|
+
// \% / \_ are matched literally instead of as wildcards.
|
|
148
|
+
const pattern = sqlLike(query);
|
|
149
|
+
const memQuery = `SELECT path, summary::text AS content, 0 AS source_order FROM "${MEMORY_TABLE}" WHERE summary::text ILIKE '%${pattern}%' ESCAPE '\\' LIMIT ${limit}`;
|
|
150
|
+
const sessQuery = `SELECT path, message::text AS content, 1 AS source_order FROM "${SESSIONS_TABLE}" WHERE message::text ILIKE '%${pattern}%' ESCAPE '\\' LIMIT ${limit}`;
|
|
151
|
+
const sql = `SELECT path, content, source_order FROM ((${memQuery}) UNION ALL (${sessQuery})) AS combined ORDER BY path, source_order LIMIT ${limit}`;
|
|
152
|
+
const rows = await dlQuery(creds, sql);
|
|
153
|
+
if (rows.length === 0) return `No matches for "${query}".`;
|
|
154
|
+
return rows
|
|
155
|
+
.map((r: any) => `[${r.path}]\n${String(r.content ?? "").slice(0, 600)}`)
|
|
156
|
+
.join("\n\n---\n\n");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// pi tools must return AgentToolResult: { content: [{type:"text", text}], details }.
|
|
160
|
+
// Returning a raw string crashes pi's renderer (render-utils.js: result.content.filter).
|
|
161
|
+
function textResult(text: string) {
|
|
162
|
+
return { content: [{ type: "text" as const, text }], details: {} };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ---------- main extension -----------------------------------------------------
|
|
166
|
+
|
|
167
|
+
const CONTEXT_PREAMBLE = `DEEPLAKE MEMORY: Persistent memory at ~/.deeplake/memory/ shared across sessions, users, and agents in your org.
|
|
168
|
+
|
|
169
|
+
Three hivemind tools are registered:
|
|
170
|
+
hivemind_search { query, limit? } keyword search across summaries + sessions
|
|
171
|
+
hivemind_read { path } read full content at a memory path
|
|
172
|
+
hivemind_index { prefix?, limit? } list summary entries
|
|
173
|
+
|
|
174
|
+
Prefer these tools — one call returns ranked hits across all summaries and sessions in a single SQL query. Different paths under /summaries/<username>/ are different users; do NOT merge or alias them. Fall back to grep on ~/.deeplake/memory/ only if tools are unavailable.`;
|
|
175
|
+
|
|
176
|
+
export default function hivemindExtension(pi: ExtensionAPI): void {
|
|
177
|
+
const captureEnabled = process.env.HIVEMIND_CAPTURE !== "false";
|
|
178
|
+
|
|
179
|
+
// --- Tools (read path) -------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
pi.registerTool({
|
|
182
|
+
name: "hivemind_search",
|
|
183
|
+
description: "Search Hivemind shared memory (summaries + raw sessions) by keyword. Use this first when the user asks about prior work or context that may exist in Hivemind. Different paths under /summaries/<username>/ are different users — do NOT merge them.",
|
|
184
|
+
parameters: {
|
|
185
|
+
type: "object",
|
|
186
|
+
properties: {
|
|
187
|
+
query: { type: "string", description: "Keyword or substring to search for." },
|
|
188
|
+
limit: { type: "number", description: "Max hits (default 10)." },
|
|
189
|
+
},
|
|
190
|
+
required: ["query"],
|
|
191
|
+
},
|
|
192
|
+
async execute(_toolCallId: string, params: { query: string; limit?: number }) {
|
|
193
|
+
const creds = loadCreds();
|
|
194
|
+
if (!creds) return textResult("Hivemind: not authenticated. Run `hivemind login` in a terminal.");
|
|
195
|
+
try {
|
|
196
|
+
return textResult(await searchTables(creds, params.query, params.limit ?? 10));
|
|
197
|
+
} catch (err: any) {
|
|
198
|
+
return textResult(`Hivemind search failed: ${err.message}`);
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
pi.registerTool({
|
|
204
|
+
name: "hivemind_read",
|
|
205
|
+
description: "Read the full content at a Hivemind memory path (e.g. /summaries/alice/abc.md or /sessions/alice/...jsonl). Use after hivemind_search to drill into a hit.",
|
|
206
|
+
parameters: {
|
|
207
|
+
type: "object",
|
|
208
|
+
properties: { path: { type: "string", description: "Absolute Hivemind memory path." } },
|
|
209
|
+
required: ["path"],
|
|
210
|
+
},
|
|
211
|
+
async execute(_toolCallId: string, params: { path: string }) {
|
|
212
|
+
const creds = loadCreds();
|
|
213
|
+
if (!creds) return textResult("Hivemind: not authenticated.");
|
|
214
|
+
const path = params.path;
|
|
215
|
+
const isSession = path.startsWith("/sessions/");
|
|
216
|
+
const table = isSession ? SESSIONS_TABLE : MEMORY_TABLE;
|
|
217
|
+
const col = isSession ? "message::text" : "summary::text";
|
|
218
|
+
const sql = `SELECT path, ${col} AS content FROM "${table}" WHERE path = '${sqlStr(path)}' LIMIT 200`;
|
|
219
|
+
try {
|
|
220
|
+
const rows = await dlQuery(creds, sql);
|
|
221
|
+
if (rows.length === 0) return textResult(`No content at ${path}.`);
|
|
222
|
+
return textResult(rows.map((r: any) => String(r.content ?? "")).join("\n"));
|
|
223
|
+
} catch (err: any) {
|
|
224
|
+
return textResult(`Hivemind read failed: ${err.message}`);
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
pi.registerTool({
|
|
230
|
+
name: "hivemind_index",
|
|
231
|
+
description: "List Hivemind summary entries (one row per session). Use to see what's in shared memory.",
|
|
232
|
+
parameters: {
|
|
233
|
+
type: "object",
|
|
234
|
+
properties: {
|
|
235
|
+
prefix: { type: "string", description: "Path prefix, e.g. '/summaries/alice/'." },
|
|
236
|
+
limit: { type: "number", description: "Max rows (default 50)." },
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
async execute(_toolCallId: string, params: { prefix?: string; limit?: number }) {
|
|
240
|
+
const creds = loadCreds();
|
|
241
|
+
if (!creds) return textResult("Hivemind: not authenticated.");
|
|
242
|
+
const where = params.prefix
|
|
243
|
+
? `WHERE path LIKE '${sqlLike(params.prefix)}%' ESCAPE '\\'`
|
|
244
|
+
: `WHERE path LIKE '/summaries/%'`;
|
|
245
|
+
const sql = `SELECT path, description, project, last_update_date FROM "${MEMORY_TABLE}" ${where} ORDER BY last_update_date DESC LIMIT ${params.limit ?? 50}`;
|
|
246
|
+
try {
|
|
247
|
+
const rows = await dlQuery(creds, sql);
|
|
248
|
+
if (rows.length === 0) return textResult("No summaries.");
|
|
249
|
+
return textResult(rows
|
|
250
|
+
.map((r: any) => `${r.path}\t${r.last_update_date}\t${r.project ?? ""}\t${r.description ?? ""}`)
|
|
251
|
+
.join("\n"));
|
|
252
|
+
} catch (err: any) {
|
|
253
|
+
return textResult(`Hivemind index failed: ${err.message}`);
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// --- Lifecycle hooks (capture path) -----------------------------------------
|
|
259
|
+
//
|
|
260
|
+
// Event shapes per pi-coding-agent/dist/core/extensions/types.d.ts:
|
|
261
|
+
// - SessionStartEvent: { type, reason, previousSessionFile? }
|
|
262
|
+
// - InputEvent: { type, text, images?, source }
|
|
263
|
+
// - ToolResultEvent: { type, toolCallId, toolName, input, content, isError, details }
|
|
264
|
+
// - MessageEndEvent: { type, message: AgentMessage }
|
|
265
|
+
// Every handler receives (event, ctx). ctx.sessionManager.getSessionId() and
|
|
266
|
+
// ctx.cwd are the canonical sources for session id + cwd — the events
|
|
267
|
+
// themselves don't carry them.
|
|
268
|
+
|
|
269
|
+
pi.on("session_start", async (_event: any, _ctx: any) => {
|
|
270
|
+
const creds = loadCreds();
|
|
271
|
+
const additional = creds
|
|
272
|
+
? `${CONTEXT_PREAMBLE}\nLogged in to Deeplake as org: ${creds.orgName ?? creds.orgId} (workspace: ${creds.workspaceId}).`
|
|
273
|
+
: `${CONTEXT_PREAMBLE}\nNot logged in to Deeplake. Run \`hivemind login\` to authenticate.`;
|
|
274
|
+
return { additionalContext: additional };
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
pi.on("input", async (event: any, ctx: any) => {
|
|
278
|
+
if (!captureEnabled) return;
|
|
279
|
+
if (event.source === "extension") return; // skip our own injected inputs
|
|
280
|
+
const creds = loadCreds();
|
|
281
|
+
if (!creds) return;
|
|
282
|
+
const text = typeof event.text === "string" ? event.text : "";
|
|
283
|
+
if (!text) return;
|
|
284
|
+
const sessionId = ctx?.sessionManager?.getSessionId?.() ?? `pi-${Date.now()}`;
|
|
285
|
+
const cwd = ctx?.cwd ?? ctx?.sessionManager?.getCwd?.() ?? process.cwd();
|
|
286
|
+
try {
|
|
287
|
+
await writeSessionRow(creds, sessionId, "pi", "input", cwd, {
|
|
288
|
+
id: crypto.randomUUID(),
|
|
289
|
+
type: "user_message",
|
|
290
|
+
session_id: sessionId,
|
|
291
|
+
content: text,
|
|
292
|
+
timestamp: new Date().toISOString(),
|
|
293
|
+
});
|
|
294
|
+
} catch { /* non-fatal */ }
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
pi.on("tool_result", async (event: any, ctx: any) => {
|
|
298
|
+
if (!captureEnabled) return;
|
|
299
|
+
const creds = loadCreds();
|
|
300
|
+
if (!creds) return;
|
|
301
|
+
const sessionId = ctx?.sessionManager?.getSessionId?.() ?? `pi-${Date.now()}`;
|
|
302
|
+
const cwd = ctx?.cwd ?? ctx?.sessionManager?.getCwd?.() ?? process.cwd();
|
|
303
|
+
// event.content is (TextContent | ImageContent)[]; extract text blocks.
|
|
304
|
+
const contentBlocks: any[] = Array.isArray(event.content) ? event.content : [];
|
|
305
|
+
const responseText = contentBlocks
|
|
306
|
+
.filter((b: any) => b?.type === "text" && typeof b.text === "string")
|
|
307
|
+
.map((b: any) => b.text)
|
|
308
|
+
.join("\n");
|
|
309
|
+
try {
|
|
310
|
+
await writeSessionRow(creds, sessionId, "pi", "tool_result", cwd, {
|
|
311
|
+
id: crypto.randomUUID(),
|
|
312
|
+
type: "tool_call",
|
|
313
|
+
session_id: sessionId,
|
|
314
|
+
tool_call_id: event.toolCallId ?? null,
|
|
315
|
+
tool_name: event.toolName ?? "unknown",
|
|
316
|
+
tool_input: JSON.stringify(event.input ?? {}),
|
|
317
|
+
tool_response: responseText || JSON.stringify(contentBlocks),
|
|
318
|
+
is_error: event.isError === true,
|
|
319
|
+
timestamp: new Date().toISOString(),
|
|
320
|
+
});
|
|
321
|
+
} catch { /* non-fatal */ }
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
pi.on("message_end", async (event: any, ctx: any) => {
|
|
325
|
+
if (!captureEnabled) return;
|
|
326
|
+
const creds = loadCreds();
|
|
327
|
+
if (!creds) return;
|
|
328
|
+
const message = event.message ?? null;
|
|
329
|
+
// AgentMessage is UserMessage | AssistantMessage | ToolResultMessage.
|
|
330
|
+
// user is captured via `input`; toolResult via `tool_result`. Only assistant here.
|
|
331
|
+
if (!message || message.role !== "assistant") return;
|
|
332
|
+
// AssistantMessage.content is (TextContent | ThinkingContent | ToolCall)[].
|
|
333
|
+
const blocks: any[] = Array.isArray(message.content) ? message.content : [];
|
|
334
|
+
const text = blocks
|
|
335
|
+
.filter((b: any) => b?.type === "text" && typeof b.text === "string")
|
|
336
|
+
.map((b: any) => b.text)
|
|
337
|
+
.join("\n");
|
|
338
|
+
if (!text) return;
|
|
339
|
+
const sessionId = ctx?.sessionManager?.getSessionId?.() ?? `pi-${Date.now()}`;
|
|
340
|
+
const cwd = ctx?.cwd ?? ctx?.sessionManager?.getCwd?.() ?? process.cwd();
|
|
341
|
+
try {
|
|
342
|
+
await writeSessionRow(creds, sessionId, "pi", "message_end", cwd, {
|
|
343
|
+
id: crypto.randomUUID(),
|
|
344
|
+
type: "assistant_message",
|
|
345
|
+
session_id: sessionId,
|
|
346
|
+
content: text,
|
|
347
|
+
timestamp: new Date().toISOString(),
|
|
348
|
+
});
|
|
349
|
+
} catch { /* non-fatal */ }
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
pi.on("session_shutdown", async (_event: any, _ctx: any) => {
|
|
353
|
+
// No-op for now. Future: trigger wiki-worker for AI summary.
|
|
354
|
+
});
|
|
355
|
+
}
|