diff --git a/client-manager.ts b/client-manager.ts new file mode 100644 index 0000000..30b9c89 --- /dev/null +++ b/client-manager.ts @@ -0,0 +1,105 @@ +import fs from "node:fs" +import os from "node:os" +import path from "node:path" +import { SupermemoryClient } from "./client.ts" +import type { SupermemoryConfig } from "./config.ts" +import { log } from "./logger.ts" + +/** + * Manages SupermemoryClient instances, one per agent. + * + * When per-agent keys are enabled, each agent can have its own API key + * stored in ~/.openclaw/agents//agent/auth-profiles.json. + * If found, a dedicated client is created for that agent. + * If not found, the global (default) client is returned. + * + * This enables multi-user setups where each user has a scoped SuperMemory + * API key for isolated memory containers. + */ +export class ClientManager { + private globalClient: SupermemoryClient + private agentClients = new Map() + private cfg: SupermemoryConfig + + constructor(globalClient: SupermemoryClient, cfg: SupermemoryConfig) { + this.globalClient = globalClient + this.cfg = cfg + } + + /** + * Get the appropriate client for the given agent. + * Returns a per-agent client if a scoped API key is found, + * otherwise returns the global client. + */ + getClient(agentId?: string): SupermemoryClient { + if (!agentId || !this.cfg.perAgentKeys) { + return this.globalClient + } + + // Check cache first + const cached = this.agentClients.get(agentId) + if (cached) { + return cached + } + + // Try to resolve a per-agent API key from auth-profiles + const agentKey = this.resolveAgentApiKey(agentId) + if (!agentKey) { + return this.globalClient + } + + // Create a per-agent client with the resolved key. + // Container tag can be agent-specific or use the scoped key's implicit container. + const containerTag = `openclaw_${agentId}` + log.info( + `creating per-agent supermemory client for "${agentId}" (container: ${containerTag})`, + ) + const client = new SupermemoryClient(agentKey, containerTag) + this.agentClients.set(agentId, client) + return client + } + + /** + * Read the supermemory API key from an agent's auth-profiles.json. + * Returns the key string if found, undefined otherwise. + */ + private resolveAgentApiKey(agentId: string): string | undefined { + const stateDir = + process.env.OPENCLAW_STATE_DIR ?? path.join(os.homedir(), ".openclaw") + const profilePath = path.join( + stateDir, + "agents", + agentId, + "agent", + "auth-profiles.json", + ) + + try { + if (!fs.existsSync(profilePath)) { + return undefined + } + + const raw = JSON.parse(fs.readFileSync(profilePath, "utf-8")) + const profiles = raw?.profiles + if (!profiles || typeof profiles !== "object") { + return undefined + } + + // Look for a supermemory profile entry + for (const [_id, profile] of Object.entries(profiles)) { + const p = profile as Record + if (p.provider === "supermemory" && p.type === "api_key") { + const key = p.key as string | undefined + if (key?.trim()) { + log.debug(`resolved per-agent supermemory key for "${agentId}"`) + return key.trim() + } + } + } + } catch (err) { + log.debug(`failed to read auth-profiles for agent "${agentId}": ${err}`) + } + + return undefined + } +} diff --git a/config.ts b/config.ts index a687aa4..af66fa5 100644 --- a/config.ts +++ b/config.ts @@ -11,6 +11,8 @@ export type SupermemoryConfig = { profileFrequency: number captureMode: CaptureMode debug: boolean + /** When true, look for per-agent API keys in auth-profiles.json. */ + perAgentKeys: boolean } const ALLOWED_KEYS = [ @@ -22,6 +24,7 @@ const ALLOWED_KEYS = [ "profileFrequency", "captureMode", "debug", + "perAgentKeys", ] function assertAllowedKeys( @@ -91,6 +94,7 @@ export function parseConfig(raw: unknown): SupermemoryConfig { ? ("everything" as const) : ("all" as const), debug: (cfg.debug as boolean) ?? false, + perAgentKeys: (cfg.perAgentKeys as boolean) ?? false, } } diff --git a/index.ts b/index.ts index 4b261b3..f17fec3 100644 --- a/index.ts +++ b/index.ts @@ -1,5 +1,6 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk" import { SupermemoryClient } from "./client.ts" +import { ClientManager } from "./client-manager.ts" import { registerCli } from "./commands/cli.ts" import { registerCommands } from "./commands/slash.ts" import { parseConfig, supermemoryConfigSchema } from "./config.ts" @@ -23,33 +24,47 @@ export default { initLogger(api.logger, cfg.debug) - const client = new SupermemoryClient(cfg.apiKey, cfg.containerTag) + const globalClient = new SupermemoryClient(cfg.apiKey, cfg.containerTag) + const clientManager = new ClientManager(globalClient, cfg) + // Helpers to track current agent context for tools and commands + let currentAgentId: string | undefined let sessionKey: string | undefined const getSessionKey = () => sessionKey + const getClient = () => clientManager.getClient(currentAgentId) - registerSearchTool(api, client, cfg) - registerStoreTool(api, client, cfg, getSessionKey) - registerForgetTool(api, client, cfg) - registerProfileTool(api, client, cfg) + registerSearchTool(api, getClient, cfg) + registerStoreTool(api, getClient, cfg, getSessionKey) + registerForgetTool(api, getClient, cfg) + registerProfileTool(api, getClient, cfg) if (cfg.autoRecall) { - const recallHandler = buildRecallHandler(client, cfg) api.on( "before_agent_start", (event: Record, ctx: Record) => { + if (ctx.agentId) currentAgentId = ctx.agentId as string if (ctx.sessionKey) sessionKey = ctx.sessionKey as string - return recallHandler(event) + const client = clientManager.getClient(currentAgentId) + return buildRecallHandler(client, cfg)(event) }, ) } if (cfg.autoCapture) { - api.on("agent_end", buildCaptureHandler(client, cfg, getSessionKey)) + api.on( + "agent_end", + (event: Record, ctx: Record) => { + if (ctx.agentId) currentAgentId = ctx.agentId as string + if (ctx.sessionKey) sessionKey = ctx.sessionKey as string + const client = clientManager.getClient(currentAgentId) + return buildCaptureHandler(client, cfg, getSessionKey)(event) + }, + ) } - registerCommands(api, client, cfg, getSessionKey) - registerCli(api, client, cfg) + // Commands and CLI use the global client (no agent context in CLI) + registerCommands(api, globalClient, cfg, getSessionKey) + registerCli(api, globalClient, cfg) api.registerService({ id: "openclaw-supermemory", diff --git a/tools/forget.ts b/tools/forget.ts index 342838b..d6cad92 100644 --- a/tools/forget.ts +++ b/tools/forget.ts @@ -6,7 +6,7 @@ import { log } from "../logger.ts" export function registerForgetTool( api: OpenClawPluginApi, - client: SupermemoryClient, + getClient: () => SupermemoryClient, _cfg: SupermemoryConfig, ): void { api.registerTool( @@ -29,7 +29,7 @@ export function registerForgetTool( ) { if (params.memoryId) { log.debug(`forget tool: direct delete id="${params.memoryId}"`) - await client.deleteMemory(params.memoryId) + await getClient().deleteMemory(params.memoryId) return { content: [{ type: "text" as const, text: "Memory forgotten." }], } @@ -37,7 +37,7 @@ export function registerForgetTool( if (params.query) { log.debug(`forget tool: search-then-delete query="${params.query}"`) - const result = await client.forgetByQuery(params.query) + const result = await getClient().forgetByQuery(params.query) return { content: [{ type: "text" as const, text: result.message }], } diff --git a/tools/profile.ts b/tools/profile.ts index d3d626c..5033ffb 100644 --- a/tools/profile.ts +++ b/tools/profile.ts @@ -6,7 +6,7 @@ import { log } from "../logger.ts" export function registerProfileTool( api: OpenClawPluginApi, - client: SupermemoryClient, + getClient: () => SupermemoryClient, _cfg: SupermemoryConfig, ): void { api.registerTool( @@ -25,7 +25,7 @@ export function registerProfileTool( async execute(_toolCallId: string, params: { query?: string }) { log.debug(`profile tool: query="${params.query ?? "(none)"}"`) - const profile = await client.getProfile(params.query) + const profile = await getClient().getProfile(params.query) if (profile.static.length === 0 && profile.dynamic.length === 0) { return { diff --git a/tools/search.ts b/tools/search.ts index 41b326e..ff698c3 100644 --- a/tools/search.ts +++ b/tools/search.ts @@ -6,7 +6,7 @@ import { log } from "../logger.ts" export function registerSearchTool( api: OpenClawPluginApi, - client: SupermemoryClient, + getClient: () => SupermemoryClient, _cfg: SupermemoryConfig, ): void { api.registerTool( @@ -28,7 +28,7 @@ export function registerSearchTool( const limit = params.limit ?? 5 log.debug(`search tool: query="${params.query}" limit=${limit}`) - const results = await client.search(params.query, limit) + const results = await getClient().search(params.query, limit) if (results.length === 0) { return { diff --git a/tools/store.ts b/tools/store.ts index 4ca2aa4..c7852c2 100644 --- a/tools/store.ts +++ b/tools/store.ts @@ -12,7 +12,7 @@ import { export function registerStoreTool( api: OpenClawPluginApi, - client: SupermemoryClient, + getClient: () => SupermemoryClient, _cfg: SupermemoryConfig, getSessionKey: () => string | undefined, ): void { @@ -35,7 +35,7 @@ export function registerStoreTool( log.debug(`store tool: category="${category}" customId="${customId}"`) - await client.addMemory( + await getClient().addMemory( params.text, { type: category, source: "openclaw_tool" }, customId,