+178
-38
@@ -1,5 +0,3 @@ | ||
| // Inline definePluginEntry to avoid openclaw module resolution issues in external plugins. | ||
| // The function just returns a descriptor object — no runtime dependency needed. | ||
| function definePluginEntry(entry) { return entry; } | ||
| import { existsSync, readFileSync } from "node:fs"; | ||
| import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs"; | ||
| import { execSync } from "node:child_process"; | ||
@@ -9,2 +7,3 @@ import { homedir } from "node:os"; | ||
| import { DeepLakeMemory } from "./memory.js"; | ||
| const API_URL = "https://api.deeplake.ai"; | ||
| function isMountActive(mountPath) { | ||
@@ -34,22 +33,98 @@ try { | ||
| } | ||
| function ensureDeeplake() { | ||
| // --- Auth state --- | ||
| let authPending = false; | ||
| let authUrl = null; | ||
| async function requestAuth() { | ||
| const resp = await fetch(`${API_URL}/auth/device/code`, { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| }); | ||
| if (!resp.ok) | ||
| throw new Error("DeepLake auth service unavailable"); | ||
| const data = await resp.json(); | ||
| authUrl = data.verification_uri_complete; | ||
| authPending = true; | ||
| // Poll in background | ||
| const pollMs = Math.max(data.interval || 5, 5) * 1000; | ||
| const deadline = Date.now() + data.expires_in * 1000; | ||
| (async () => { | ||
| while (Date.now() < deadline && authPending) { | ||
| await new Promise(r => setTimeout(r, pollMs)); | ||
| try { | ||
| const tokenResp = await fetch(`${API_URL}/auth/device/token`, { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ device_code: data.device_code }), | ||
| }); | ||
| if (tokenResp.ok) { | ||
| const tokenData = await tokenResp.json(); | ||
| const token = tokenData.access_token; | ||
| // Get orgs and pick personal org | ||
| const orgsResp = await fetch(`${API_URL}/organizations`, { | ||
| headers: { Authorization: `Bearer ${token}`, "X-Deeplake-Client": "cli" }, | ||
| }); | ||
| let orgId = ""; | ||
| if (orgsResp.ok) { | ||
| const orgs = await orgsResp.json(); | ||
| const personal = orgs.find(o => o.name.endsWith("'s Organization")); | ||
| orgId = personal?.id ?? orgs[0]?.id ?? ""; | ||
| } | ||
| // Create long-lived API token | ||
| let savedToken = token; | ||
| if (orgId) { | ||
| try { | ||
| const apiTokenResp = await fetch(`${API_URL}/users/me/tokens`, { | ||
| method: "POST", | ||
| headers: { | ||
| Authorization: `Bearer ${token}`, | ||
| "Content-Type": "application/json", | ||
| "X-Activeloop-Org-Id": orgId, | ||
| }, | ||
| body: JSON.stringify({ name: `plur1bus-${new Date().toISOString().split("T")[0]}`, duration: 365 * 24 * 60 * 60, organization_id: orgId }), | ||
| }); | ||
| if (apiTokenResp.ok) { | ||
| const data = await apiTokenResp.json(); | ||
| savedToken = typeof data.token === "string" ? data.token : data.token.token; | ||
| } | ||
| } | ||
| catch { } | ||
| } | ||
| // Save credentials directly | ||
| const deeplakeDir = join(homedir(), ".deeplake"); | ||
| mkdirSync(deeplakeDir, { recursive: true }); | ||
| writeFileSync(join(deeplakeDir, "credentials.json"), JSON.stringify({ | ||
| token: savedToken, | ||
| orgId, | ||
| apiUrl: API_URL, | ||
| savedAt: new Date().toISOString(), | ||
| })); | ||
| authPending = false; | ||
| authUrl = null; | ||
| return; | ||
| } | ||
| } | ||
| catch { } | ||
| } | ||
| authPending = false; | ||
| authUrl = null; | ||
| })(); | ||
| return data.verification_uri_complete; | ||
| } | ||
| function installCli() { | ||
| const deeplakeDir = join(homedir(), ".deeplake"); | ||
| // 1. CLI installed? | ||
| if (!existsSync(join(deeplakeDir, "cli.js"))) { | ||
| execSync("curl -fsSL https://deeplake.ai/install.sh | bash", { | ||
| stdio: "inherit", | ||
| timeout: 120000, | ||
| }); | ||
| execSync("curl -fsSL https://deeplake.ai/install.sh | bash", { stdio: "ignore", timeout: 120000 }); | ||
| } | ||
| } | ||
| async function initAndMount() { | ||
| const deeplakeDir = join(homedir(), ".deeplake"); | ||
| const node = join(deeplakeDir, "node"); | ||
| const cli = join(deeplakeDir, "cli.js"); | ||
| // 2. Logged in? | ||
| if (!existsSync(join(deeplakeDir, "credentials.json"))) { | ||
| execSync(`${node} ${cli} login`, { stdio: "inherit", timeout: 120000 }); | ||
| } | ||
| // 3. Has a mount? | ||
| let mountPath = findDeeplakeMount(); | ||
| if (mountPath) | ||
| return mountPath; | ||
| // No active mount — check if any registered | ||
| if (!existsSync(cli)) | ||
| return null; | ||
| const credsPath = join(deeplakeDir, "credentials.json"); | ||
| if (!existsSync(credsPath)) | ||
| return null; | ||
| const creds = JSON.parse(readFileSync(credsPath, "utf-8")); | ||
| // Try mounting existing | ||
| const mountsFile = join(deeplakeDir, "mounts.json"); | ||
@@ -60,27 +135,82 @@ if (existsSync(mountsFile)) { | ||
| if (mounts.length > 0) { | ||
| // Mount the first registered one | ||
| execSync(`${node} ${cli} mount ${mounts[0].mountPath}`, { | ||
| stdio: "inherit", | ||
| timeout: 120000, | ||
| }); | ||
| mountPath = findDeeplakeMount(); | ||
| if (mountPath) | ||
| return mountPath; | ||
| try { | ||
| execSync(`${node} ${cli} mount ${mounts[0].mountPath}`, { stdio: "ignore", timeout: 60000 }); | ||
| const m = findDeeplakeMount(); | ||
| if (m) | ||
| return m; | ||
| } | ||
| catch { } | ||
| } | ||
| } | ||
| // No mounts at all — init one | ||
| execSync(`${node} ${cli} init`, { stdio: "inherit", timeout: 120000 }); | ||
| mountPath = findDeeplakeMount(); | ||
| if (mountPath) | ||
| return mountPath; | ||
| throw new Error("DeepLake setup completed but no active mount found. Run: deeplake mount --all"); | ||
| // No mounts — create table via API and register mount directly | ||
| const defaultMount = join(homedir(), "deeplake"); | ||
| const tableName = "deeplake_memory"; | ||
| const dbUrl = `deeplake://${creds.orgId}/default/${tableName}`; | ||
| // Create table via API (ignore if exists) | ||
| try { | ||
| await fetch(`${API_URL}/workspaces/default/tables`, { | ||
| method: "POST", | ||
| headers: { | ||
| Authorization: `Bearer ${creds.token}`, | ||
| "Content-Type": "application/json", | ||
| "X-Activeloop-Org-Id": creds.orgId, | ||
| }, | ||
| body: JSON.stringify({ table_name: tableName, table_schema: { | ||
| _id: "TEXT", content: "BYTEA", content_text: "TEXT", | ||
| filename: "TEXT", mime_type: "TEXT", path: "TEXT", size_bytes: "BIGINT", | ||
| } }), | ||
| }); | ||
| } | ||
| catch { } | ||
| // Register mount in mounts.json | ||
| mkdirSync(defaultMount, { recursive: true }); | ||
| const mountEntry = { | ||
| mountPath: defaultMount, | ||
| dbUrl, | ||
| createdAt: new Date().toISOString(), | ||
| pid: null, | ||
| pidFile: join(deeplakeDir, "pid_" + defaultMount.replace(/\//g, "_") + ".pid"), | ||
| backendType: "managed", | ||
| tableName, | ||
| workspaceName: "default", | ||
| }; | ||
| writeFileSync(mountsFile, JSON.stringify({ mounts: [mountEntry] }, null, 2)); | ||
| // Mount it | ||
| try { | ||
| execSync(`${node} ${cli} mount ${defaultMount}`, { stdio: "ignore", timeout: 60000 }); | ||
| return findDeeplakeMount(); | ||
| } | ||
| catch { } | ||
| return null; | ||
| } | ||
| let memory = null; | ||
| function getMemory(config) { | ||
| if (!memory) { | ||
| const mountPath = config.mountPath ?? findDeeplakeMount() ?? ensureDeeplake(); | ||
| async function getMemory(config) { | ||
| if (memory) | ||
| return memory; | ||
| // Already mounted? | ||
| let mountPath = config.mountPath ?? findDeeplakeMount(); | ||
| if (mountPath) { | ||
| memory = new DeepLakeMemory(mountPath); | ||
| memory.init(); | ||
| return memory; | ||
| } | ||
| return memory; | ||
| // CLI installed? | ||
| installCli(); | ||
| // Credentials? | ||
| const deeplakeDir = join(homedir(), ".deeplake"); | ||
| if (!existsSync(join(deeplakeDir, "credentials.json"))) { | ||
| // Need auth — start device flow if not already pending | ||
| if (!authPending) { | ||
| await requestAuth(); | ||
| } | ||
| return null; // Caller handles the auth URL | ||
| } | ||
| // Have credentials but no mount — init + mount | ||
| mountPath = await initAndMount(); | ||
| if (mountPath) { | ||
| memory = new DeepLakeMemory(mountPath); | ||
| memory.init(); | ||
| return memory; | ||
| } | ||
| return null; | ||
| } | ||
@@ -101,3 +231,11 @@ export default definePluginEntry({ | ||
| try { | ||
| const m = getMemory(config); | ||
| const m = await getMemory(config); | ||
| // Auth needed — send URL to user | ||
| if (!m && authUrl) { | ||
| return { | ||
| prependContext: `\n\n🔐 DeepLake memory requires authentication.\n\nSign in here: ${authUrl}\n\nAfter signing in, send your message again.\n`, | ||
| }; | ||
| } | ||
| if (!m) | ||
| return; | ||
| const stopWords = new Set(["the", "and", "for", "are", "but", "not", "you", "all", "can", "had", "her", "was", "one", "our", "out", "has", "have", "what", "does", "like", "with", "this", "that", "from", "they", "been", "will", "more", "when", "who", "how", "its", "into", "some", "than", "them", "these", "then", "your", "just", "about", "would", "could", "should", "where", "which", "there", "their", "being", "each", "other"]); | ||
@@ -141,3 +279,5 @@ const words = event.prompt.toLowerCase() | ||
| try { | ||
| const m = getMemory(config); | ||
| const m = await getMemory(config); | ||
| if (!m) | ||
| return; | ||
| const texts = []; | ||
@@ -144,0 +284,0 @@ for (const msg of ev.messages) { |
+1
-1
| { | ||
| "name": "plur1bus", | ||
| "version": "0.1.1", | ||
| "version": "0.2.0", | ||
| "type": "module", | ||
@@ -5,0 +5,0 @@ "description": "Plur1bus — cloud-backed persistent memory for AI agents, powered by DeepLake", |
Network access
Supply chain riskThis module accesses the network.
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
21309
37.52%479
41.3%7
250%