Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions client-manager.ts
Original file line number Diff line number Diff line change
@@ -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/<agentId>/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<string, SupermemoryClient>()
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<string, unknown>
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
}
}
4 changes: 4 additions & 0 deletions config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand All @@ -22,6 +24,7 @@ const ALLOWED_KEYS = [
"profileFrequency",
"captureMode",
"debug",
"perAgentKeys",
]

function assertAllowedKeys(
Expand Down Expand Up @@ -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,
}
}

Expand Down
35 changes: 25 additions & 10 deletions index.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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<string, unknown>, ctx: Record<string, unknown>) => {
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<string, unknown>, ctx: Record<string, unknown>) => {
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",
Expand Down
6 changes: 3 additions & 3 deletions tools/forget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { log } from "../logger.ts"

export function registerForgetTool(
api: OpenClawPluginApi,
client: SupermemoryClient,
getClient: () => SupermemoryClient,
_cfg: SupermemoryConfig,
): void {
api.registerTool(
Expand All @@ -29,15 +29,15 @@ 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." }],
}
}

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 }],
}
Expand Down
4 changes: 2 additions & 2 deletions tools/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { log } from "../logger.ts"

export function registerProfileTool(
api: OpenClawPluginApi,
client: SupermemoryClient,
getClient: () => SupermemoryClient,
_cfg: SupermemoryConfig,
): void {
api.registerTool(
Expand All @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions tools/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { log } from "../logger.ts"

export function registerSearchTool(
api: OpenClawPluginApi,
client: SupermemoryClient,
getClient: () => SupermemoryClient,
_cfg: SupermemoryConfig,
): void {
api.registerTool(
Expand All @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions tools/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {

export function registerStoreTool(
api: OpenClawPluginApi,
client: SupermemoryClient,
getClient: () => SupermemoryClient,
_cfg: SupermemoryConfig,
getSessionKey: () => string | undefined,
): void {
Expand All @@ -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,
Expand Down