diff --git a/.gitignore b/.gitignore
index 59be53c..9763a2b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@ dist/
.arch/
lib/*.ts
!lib/*.d.ts
+temp/
*.tsbuildinfo
.DS_Store
diff --git a/README.md b/README.md
index cb9910f..dd419cf 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,11 @@
-# Supermemory Plugin for OpenClaw (previously Clawdbot)
-
-
+# OpenClaw Supermemory Plugin
+
Long-term memory for OpenClaw. Automatically remembers conversations, recalls relevant context, and builds a persistent user profile — all powered by [Supermemory](https://supermemory.ai) cloud. No local infrastructure required.
-> **✨ Requires [Supermemory Pro or above](https://console.supermemory.ai/billing)** - Unlock the state of the art memory for your OpenClaw bot.
+> **Requires [Supermemory Pro or above](https://console.supermemory.ai/billing)** - Unlock the state of the art memory for your OpenClaw bot.
## Install
@@ -16,76 +15,113 @@ openclaw plugins install @supermemory/openclaw-supermemory
Restart OpenClaw after installing.
-## Configuration
-
-The only required value is your Supermemory API key. Get one at [console.supermemory.ai](https://console.supermemory.ai).
-
-Set it as an environment variable:
+## Setup
```bash
-export SUPERMEMORY_OPENCLAW_API_KEY="sm_..."
+openclaw supermemory setup
```
-Or configure it directly in `openclaw.json`:
+Enter your API key from [console.supermemory.ai](https://console.supermemory.ai). That's it.
-```json5
-{
- "plugins": {
- "entries": {
- "openclaw-supermemory": {
- "enabled": true,
- "config": {
- "apiKey": "${SUPERMEMORY_OPENCLAW_API_KEY}"
- }
- }
- }
- }
-}
-```
+### Advanced Setup
-### Advanced options
+```bash
+openclaw supermemory setup-advanced
+```
-| Key | Type | Default | Description |
-|-----|------|---------|-------------|
-| `containerTag` | `string` | `openclaw_{hostname}` | Memory namespace. All channels share this tag. |
-| `autoRecall` | `boolean` | `true` | Inject relevant memories before every AI turn. |
-| `autoCapture` | `boolean` | `true` | Automatically store conversation content after every turn. |
-| `maxRecallResults` | `number` | `10` | Max memories injected into context per turn. |
-| `profileFrequency` | `number` | `50` | Inject full user profile every N turns. Search results are injected every turn. |
-| `captureMode` | `string` | `"all"` | `"all"` filters short texts and injected context. `"everything"` captures all messages. |
-| `debug` | `boolean` | `false` | Verbose debug logs for API calls and responses. |
+Configure all options interactively: container tag, auto-recall, auto-capture, capture mode, custom container tags, and more.
## How it works
-Once installed, the plugin works automatically with zero interaction:
+Once installed, the plugin works automatically:
-- **Auto-Recall** — Before every AI turn, the plugin queries Supermemory for relevant memories and injects them as context. The AI sees your user profile (preferences, facts) and semantically similar past conversations.
-- **Auto-Capture** — After every AI turn, the last user/assistant exchange is sent to Supermemory for extraction and long-term storage.
+- **Auto-Recall** — Before every AI turn, queries Supermemory for relevant memories and injects them as context. The AI sees your user profile and semantically similar past conversations.
+- **Auto-Capture** — After every AI turn, the conversation is sent to Supermemory for extraction and long-term storage.
+- **Custom Container Tags** — Define custom memory containers (e.g., `work`, `personal`, `bookmarks`). The AI automatically picks the right container based on your instructions when using memory tools.
-Everything runs in the cloud. Supermemory handles extraction, deduplication, and profile building on its end.
+Everything runs in the cloud. Supermemory handles extraction, deduplication, and profile building.
## Slash Commands
-| Command | Description |
-|---------|-------------|
-| `/remember ` | Manually save something to memory. |
-| `/recall ` | Search your memories and see results with similarity scores. |
+| Command | Description |
+| ------------------ | --------------------------------------- |
+| `/remember ` | Manually save something to memory. |
+| `/recall ` | Search memories with similarity scores. |
## AI Tools
-The AI can use these tools autonomously during conversations:
+The AI uses these tools autonomously. With custom container tags enabled, all tools support a `containerTag` parameter for routing to specific containers.
-| Tool | Description |
-|------|-------------|
-| `supermemory_store` | Save information to long-term memory. |
-| `supermemory_search` | Search memories by query. |
-| `supermemory_forget` | Delete a memory by query. |
-| `supermemory_profile` | View the user profile (persistent facts + recent context). |
+| Tool | Description |
+| --------------------- | ------------------------------------------------------ |
+| `supermemory_store` | Save information to memory. |
+| `supermemory_search` | Search memories by query. |
+| `supermemory_forget` | Delete a memory by query or ID. |
+| `supermemory_profile` | View user profile (persistent facts + recent context). |
## CLI Commands
```bash
-openclaw supermemory search # Search memories
-openclaw supermemory profile # View user profile
-openclaw supermemory wipe # Delete all memories (destructive, requires confirmation)
+openclaw supermemory setup # Configure API key
+openclaw supermemory setup-advanced # Configure all options
+openclaw supermemory status # View current configuration
+openclaw supermemory search # Search memories
+openclaw supermemory profile # View user profile
+openclaw supermemory wipe # Delete all memories (requires confirmation)
+```
+
+## Configuration
+
+Set API key via environment variable:
+
+```bash
+export SUPERMEMORY_OPENCLAW_API_KEY="sm_..."
+```
+
+Or configure in `~/.openclaw/openclaw.json`:
+
+### Options
+
+| Key | Type | Default | Description |
+| ----------------------------- | --------- | --------------------- | --------------------------------------------------------- |
+| `apiKey` | `string` | — | Supermemory API key. |
+| `containerTag` | `string` | `openclaw_{hostname}` | Root memory namespace. |
+| `autoRecall` | `boolean` | `true` | Inject relevant memories before every AI turn. |
+| `autoCapture` | `boolean` | `true` | Store conversations after every turn. |
+| `maxRecallResults` | `number` | `10` | Max memories injected per turn. |
+| `profileFrequency` | `number` | `50` | Inject full profile every N turns. |
+| `captureMode` | `string` | `"all"` | `"all"` filters short texts, `"everything"` captures all. |
+| `debug` | `boolean` | `false` | Verbose debug logs. |
+| `enableCustomContainerTags` | `boolean` | `false` | Enable custom container routing. |
+| `customContainers` | `array` | `[]` | Custom containers with `tag` and `description`. |
+| `customContainerInstructions` | `string` | `""` | Instructions for AI on container routing. |
+
+### Full Example
+
+```json
+{
+ "plugins": {
+ "entries": {
+ "openclaw-supermemory": {
+ "enabled": true,
+ "config": {
+ "apiKey": "${SUPERMEMORY_OPENCLAW_API_KEY}",
+ "containerTag": "my_memory",
+ "autoRecall": true,
+ "autoCapture": true,
+ "maxRecallResults": 10,
+ "profileFrequency": 50,
+ "captureMode": "all",
+ "debug": false,
+ "enableCustomContainerTags": true,
+ "customContainers": [
+ { "tag": "work", "description": "Work-related memories" },
+ { "tag": "personal", "description": "Personal notes" }
+ ],
+ "customContainerInstructions": "Store work tasks in 'work', personal stuff in 'personal'"
+ }
+ }
+ }
+ }
+}
```
diff --git a/client.ts b/client.ts
index afee359..df5cac2 100644
--- a/client.ts
+++ b/client.ts
@@ -61,18 +61,21 @@ export class SupermemoryClient {
content: string,
metadata?: Record,
customId?: string,
+ containerTag?: string,
): Promise<{ id: string }> {
const cleaned = sanitizeContent(content)
+ const tag = containerTag ?? this.containerTag
log.debugRequest("add", {
contentLength: cleaned.length,
customId,
metadata,
+ containerTag: tag,
})
const result = await this.client.add({
content: cleaned,
- containerTag: this.containerTag,
+ containerTag: tag,
...(metadata && { metadata }),
...(customId && { customId }),
})
@@ -81,16 +84,22 @@ export class SupermemoryClient {
return { id: result.id }
}
- async search(query: string, limit = 5): Promise {
+ async search(
+ query: string,
+ limit = 5,
+ containerTag?: string,
+ ): Promise {
+ const tag = containerTag ?? this.containerTag
+
log.debugRequest("search.memories", {
query,
limit,
- containerTag: this.containerTag,
+ containerTag: tag,
})
const response = await this.client.search.memories({
q: query,
- containerTag: this.containerTag,
+ containerTag: tag,
limit,
})
@@ -106,11 +115,16 @@ export class SupermemoryClient {
return results
}
- async getProfile(query?: string): Promise {
- log.debugRequest("profile", { containerTag: this.containerTag, query })
+ async getProfile(
+ query?: string,
+ containerTag?: string,
+ ): Promise {
+ const tag = containerTag ?? this.containerTag
+
+ log.debugRequest("profile", { containerTag: tag, query })
const response = await this.client.profile({
- containerTag: this.containerTag,
+ containerTag: tag,
...(query && { q: query }),
})
@@ -131,13 +145,18 @@ export class SupermemoryClient {
return result
}
- async deleteMemory(id: string): Promise<{ id: string; forgotten: boolean }> {
+ async deleteMemory(
+ id: string,
+ containerTag?: string,
+ ): Promise<{ id: string; forgotten: boolean }> {
+ const tag = containerTag ?? this.containerTag
+
log.debugRequest("memories.delete", {
id,
- containerTag: this.containerTag,
+ containerTag: tag,
})
const result = await this.client.memories.forget({
- containerTag: this.containerTag,
+ containerTag: tag,
id,
})
log.debugResponse("memories.delete", result)
@@ -146,16 +165,17 @@ export class SupermemoryClient {
async forgetByQuery(
query: string,
+ containerTag?: string,
): Promise<{ success: boolean; message: string }> {
- log.debugRequest("forgetByQuery", { query })
+ log.debugRequest("forgetByQuery", { query, containerTag })
- const results = await this.search(query, 5)
+ const results = await this.search(query, 5, containerTag)
if (results.length === 0) {
return { success: false, message: "No matching memory found to forget." }
}
const target = results[0]
- await this.deleteMemory(target.id)
+ await this.deleteMemory(target.id, containerTag)
const preview = limitText(target.content || target.memory || "", 100)
return { success: true, message: `Forgot: "${preview}"` }
diff --git a/commands/cli.ts b/commands/cli.ts
index 09df7d7..7cfef62 100644
--- a/commands/cli.ts
+++ b/commands/cli.ts
@@ -1,8 +1,397 @@
+import * as fs from "node:fs"
+import * as os from "node:os"
+import * as path from "node:path"
+import * as readline from "node:readline"
import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
import type { SupermemoryClient } from "../client.ts"
import type { SupermemoryConfig } from "../config.ts"
import { log } from "../logger.ts"
+export function registerCliSetup(api: OpenClawPluginApi): void {
+ api.registerCli(
+ // biome-ignore lint/suspicious/noExplicitAny: openclaw SDK does not ship types
+ ({ program }: { program: any }) => {
+ const cmd = program
+ .command("supermemory")
+ .description("Supermemory long-term memory commands")
+
+ cmd
+ .command("setup")
+ .description("Configure Supermemory API key")
+ .action(async () => {
+ const configDir = path.join(os.homedir(), ".openclaw")
+ const configPath = path.join(configDir, "openclaw.json")
+
+ console.log("\n🧠 Supermemory Setup\n")
+ console.log("Get your API key from: https://console.supermemory.ai\n")
+
+ const rl = readline.createInterface({
+ input: process.stdin,
+ output: process.stdout,
+ })
+
+ const apiKey = await new Promise((resolve) => {
+ rl.question("Enter your Supermemory API key: ", resolve)
+ })
+ rl.close()
+
+ if (!apiKey.trim()) {
+ console.log("\nNo API key provided. Setup cancelled.")
+ return
+ }
+
+ if (!apiKey.startsWith("sm_")) {
+ console.log("\nWarning: API key should start with 'sm_'")
+ }
+
+ let config: Record = {}
+ if (fs.existsSync(configPath)) {
+ try {
+ config = JSON.parse(fs.readFileSync(configPath, "utf-8"))
+ } catch {
+ config = {}
+ }
+ }
+
+ if (!config.plugins) config.plugins = {}
+ const plugins = config.plugins as Record
+ if (!plugins.entries) plugins.entries = {}
+ const entries = plugins.entries as Record
+
+ entries["openclaw-supermemory"] = {
+ enabled: true,
+ config: {
+ apiKey: apiKey.trim(),
+ },
+ }
+
+ if (!fs.existsSync(configDir)) {
+ fs.mkdirSync(configDir, { recursive: true })
+ }
+
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2))
+
+ console.log("\n✓ API key saved to ~/.openclaw/openclaw.json")
+ console.log(
+ " Restart OpenClaw to apply changes: openclaw gateway --force\n",
+ )
+ })
+
+ cmd
+ .command("setup-advanced")
+ .description("Configure Supermemory with all options")
+ .action(async () => {
+ const configDir = path.join(os.homedir(), ".openclaw")
+ const configPath = path.join(configDir, "openclaw.json")
+ const defaultTag = os.hostname().replace(/[^a-zA-Z0-9_]/g, "_")
+
+ console.log("\n🧠 Supermemory Advanced Setup\n")
+ console.log("Press Enter to use default values shown in [brackets]\n")
+ console.log("Get your API key from: https://console.supermemory.ai\n")
+
+ const rl = readline.createInterface({
+ input: process.stdin,
+ output: process.stdout,
+ })
+
+ const ask = (question: string): Promise =>
+ new Promise((resolve) => rl.question(question, resolve))
+
+ const apiKey = await ask("API key (required): ")
+ if (!apiKey.trim()) {
+ console.log("\nNo API key provided. Setup cancelled.")
+ rl.close()
+ return
+ }
+
+ if (!apiKey.startsWith("sm_")) {
+ console.log("Warning: API key should start with 'sm_'\n")
+ }
+
+ const containerTag = await ask(
+ `Container tag [openclaw_${defaultTag}]: `,
+ )
+
+ console.log("\nAuto-recall:")
+ console.log(
+ " true - Inject relevant memories before each AI response (recommended)",
+ )
+ console.log(" false - Disable automatic memory recall")
+ const autoRecallInput = await ask("Auto-recall (true/false) [true]: ")
+ let autoRecall = true
+ if (autoRecallInput.trim().toLowerCase() === "false") {
+ autoRecall = false
+ } else if (
+ autoRecallInput.trim() &&
+ autoRecallInput.trim().toLowerCase() !== "true"
+ ) {
+ console.log(" Invalid value, using default: true")
+ }
+
+ console.log("\nAuto-capture:")
+ console.log(
+ " true - Save conversations to memory after each AI response (recommended)",
+ )
+ console.log(" false - Disable automatic conversation capture")
+ const autoCaptureInput = await ask(
+ "Auto-capture (true/false) [true]: ",
+ )
+ let autoCapture = true
+ if (autoCaptureInput.trim().toLowerCase() === "false") {
+ autoCapture = false
+ } else if (
+ autoCaptureInput.trim() &&
+ autoCaptureInput.trim().toLowerCase() !== "true"
+ ) {
+ console.log(" Invalid value, using default: true")
+ }
+
+ const maxResultsInput = await ask(
+ "Max memories to recall per turn (1-20) [10]: ",
+ )
+ let maxRecallResults = 10
+ const parsedMax = Number.parseInt(maxResultsInput.trim(), 10)
+ if (maxResultsInput.trim()) {
+ if (parsedMax >= 1 && parsedMax <= 20) {
+ maxRecallResults = parsedMax
+ } else {
+ console.log(" Invalid value, using default: 10")
+ }
+ }
+
+ const profileFreqInput = await ask(
+ "Inject full profile every N turns (1-500) [50]: ",
+ )
+ let profileFrequency = 50
+ const parsedFreq = Number.parseInt(profileFreqInput.trim(), 10)
+ if (profileFreqInput.trim()) {
+ if (parsedFreq >= 1 && parsedFreq <= 500) {
+ profileFrequency = parsedFreq
+ } else {
+ console.log(" Invalid value, using default: 50")
+ }
+ }
+
+ console.log("\nCapture mode:")
+ console.log(
+ " all - Filter short texts and context blocks (recommended)",
+ )
+ console.log(" everything - Capture all messages without filtering")
+ const captureModeInput = await ask(
+ "Capture mode (all/everything) [all]: ",
+ )
+ let captureMode: "all" | "everything" = "all"
+ if (captureModeInput.trim().toLowerCase() === "everything") {
+ captureMode = "everything"
+ } else if (
+ captureModeInput.trim() &&
+ captureModeInput.trim().toLowerCase() !== "all"
+ ) {
+ console.log(" Invalid value, using default: all")
+ }
+
+ console.log("\n--- Custom Container Tags (Advanced) ---")
+ console.log("Define custom containers for AI-driven memory routing.")
+ const enableCustomContainerTagsInput = await ask(
+ "Enable custom container tags? (true/false) [false]: ",
+ )
+ let enableCustomContainerTags = false
+ if (enableCustomContainerTagsInput.trim().toLowerCase() === "true") {
+ enableCustomContainerTags = true
+ } else if (
+ enableCustomContainerTagsInput.trim() &&
+ enableCustomContainerTagsInput.trim().toLowerCase() !== "false"
+ ) {
+ console.log(" Invalid value, using default: false")
+ }
+
+ console.log(
+ "\nAdd custom containers (tag:description). Leave blank when done.",
+ )
+ const customContainers: Array<{ tag: string; description: string }> =
+ []
+ while (true) {
+ const containerInput = await ask(
+ "Container (e.g. work:Work projects): ",
+ )
+ if (!containerInput.trim()) break
+ const [tag, ...descParts] = containerInput.split(":")
+ const description = descParts.join(":").trim()
+ if (tag && description) {
+ customContainers.push({
+ tag: tag.trim().replace(/[^a-zA-Z0-9_]/g, "_"),
+ description,
+ })
+ console.log(` Added: ${tag.trim()} → ${description}`)
+ } else {
+ console.log(" Invalid format. Use tag:description")
+ }
+ }
+
+ const customContainerInstructions = await ask(
+ "Custom container tag instructions (optional): ",
+ )
+
+ rl.close()
+
+ let config: Record = {}
+ if (fs.existsSync(configPath)) {
+ try {
+ config = JSON.parse(fs.readFileSync(configPath, "utf-8"))
+ } catch {
+ config = {}
+ }
+ }
+
+ if (!config.plugins) config.plugins = {}
+ const plugins = config.plugins as Record
+ if (!plugins.entries) plugins.entries = {}
+ const entries = plugins.entries as Record
+
+ const pluginConfig: Record = {
+ apiKey: apiKey.trim(),
+ }
+
+ if (containerTag.trim()) {
+ pluginConfig.containerTag = containerTag
+ .trim()
+ .replace(/[^a-zA-Z0-9_]/g, "_")
+ }
+ if (!autoRecall) pluginConfig.autoRecall = false
+ if (!autoCapture) pluginConfig.autoCapture = false
+ if (maxRecallResults !== 10)
+ pluginConfig.maxRecallResults = maxRecallResults
+ if (profileFrequency !== 50)
+ pluginConfig.profileFrequency = profileFrequency
+ if (captureMode !== "all") pluginConfig.captureMode = captureMode
+ if (enableCustomContainerTags)
+ pluginConfig.enableCustomContainerTags = true
+ if (customContainerInstructions.trim()) {
+ pluginConfig.customContainerInstructions =
+ customContainerInstructions.trim()
+ }
+ if (customContainers.length > 0) {
+ pluginConfig.customContainers = customContainers
+ }
+
+ entries["openclaw-supermemory"] = {
+ enabled: true,
+ config: pluginConfig,
+ }
+
+ if (!fs.existsSync(configDir)) {
+ fs.mkdirSync(configDir, { recursive: true })
+ }
+
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2))
+
+ console.log("\n✓ Configuration saved to ~/.openclaw/openclaw.json")
+ console.log("\nSettings:")
+ console.log(
+ ` API key: ${apiKey.slice(0, 8)}...${apiKey.slice(-4)}`,
+ )
+ console.log(
+ ` Container tag: ${containerTag.trim() || `openclaw_${defaultTag}`}`,
+ )
+ console.log(` Auto-recall: ${autoRecall}`)
+ console.log(` Auto-capture: ${autoCapture}`)
+ console.log(` Max results: ${maxRecallResults}`)
+ console.log(` Profile freq: ${profileFrequency}`)
+ console.log(` Capture mode: ${captureMode}`)
+ console.log(
+ ` Custom containers: ${enableCustomContainerTags ? "enabled" : "disabled"}`,
+ )
+ console.log(` Custom containers: ${customContainers.length}`)
+ if (customContainerInstructions.trim()) {
+ console.log(
+ ` Routing instructions: "${customContainerInstructions.trim().slice(0, 50)}${customContainerInstructions.length > 50 ? "..." : ""}"`,
+ )
+ }
+ console.log("\nRestart OpenClaw to apply: openclaw gateway --force\n")
+ })
+
+ cmd
+ .command("status")
+ .description("Check Supermemory configuration status")
+ .action(async () => {
+ const configPath = path.join(
+ os.homedir(),
+ ".openclaw",
+ "openclaw.json",
+ )
+ const envKey = process.env.SUPERMEMORY_OPENCLAW_API_KEY
+ const defaultTag = `openclaw_${os.hostname().replace(/[^a-zA-Z0-9_]/g, "_")}`
+
+ console.log("\n🧠 Supermemory Status\n")
+
+ let apiKeySource = ""
+ let apiKeyDisplay = ""
+ let pluginConfig: Record = {}
+ let enabled = true
+
+ if (envKey) {
+ apiKeySource = "environment"
+ apiKeyDisplay = `${envKey.slice(0, 8)}...${envKey.slice(-4)}`
+ }
+
+ if (fs.existsSync(configPath)) {
+ try {
+ const config = JSON.parse(fs.readFileSync(configPath, "utf-8"))
+ const entry = config?.plugins?.entries?.["openclaw-supermemory"]
+ if (entry) {
+ enabled = entry.enabled ?? true
+ pluginConfig = entry.config ?? {}
+ if (pluginConfig.apiKey && !envKey) {
+ const key = pluginConfig.apiKey as string
+ apiKeySource = "config"
+ apiKeyDisplay = `${key.slice(0, 8)}...${key.slice(-4)}`
+ }
+ }
+ } catch {
+ console.log("✗ Could not read config file\n")
+ return
+ }
+ }
+
+ if (!apiKeyDisplay) {
+ console.log("✗ No API key configured")
+ console.log(" Run: openclaw supermemory setup\n")
+ return
+ }
+
+ const customContainers = Array.isArray(pluginConfig.customContainers)
+ ? pluginConfig.customContainers
+ : []
+
+ console.log(
+ `✓ API key: ${apiKeyDisplay} (from ${apiKeySource})`,
+ )
+ console.log(` Enabled: ${enabled}`)
+ console.log(
+ ` Container tag: ${pluginConfig.containerTag ?? defaultTag}`,
+ )
+ console.log(` Auto-recall: ${pluginConfig.autoRecall ?? true}`)
+ console.log(` Auto-capture: ${pluginConfig.autoCapture ?? true}`)
+ console.log(
+ ` Max results: ${pluginConfig.maxRecallResults ?? 10}`,
+ )
+ console.log(
+ ` Profile freq: ${pluginConfig.profileFrequency ?? 50}`,
+ )
+ console.log(
+ ` Capture mode: ${pluginConfig.captureMode ?? "all"}`,
+ )
+ console.log(
+ ` Custom containers: ${pluginConfig.enableCustomContainerTags ? "enabled" : "disabled"}`,
+ )
+ console.log(` Custom containers: ${customContainers.length}`)
+ console.log("")
+ })
+ },
+ { commands: ["supermemory"] },
+ )
+}
+
export function registerCli(
api: OpenClawPluginApi,
client: SupermemoryClient,
@@ -11,9 +400,11 @@ export function registerCli(
api.registerCli(
// biome-ignore lint/suspicious/noExplicitAny: openclaw SDK does not ship types
({ program }: { program: any }) => {
- const cmd = program
- .command("supermemory")
- .description("Supermemory long-term memory commands")
+ const cmd = program.commands.find(
+ // biome-ignore lint/suspicious/noExplicitAny: openclaw SDK does not ship types
+ (c: any) => c.name() === "supermemory",
+ )
+ if (!cmd) return
cmd
.command("search")
@@ -67,7 +458,6 @@ export function registerCli(
.description("Delete ALL memories for this container tag")
.action(async () => {
const tag = client.getContainerTag()
- const readline = await import("node:readline")
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
diff --git a/commands/slash.ts b/commands/slash.ts
index f9dc795..48f437a 100644
--- a/commands/slash.ts
+++ b/commands/slash.ts
@@ -4,6 +4,32 @@ import type { SupermemoryConfig } from "../config.ts"
import { log } from "../logger.ts"
import { buildDocumentId, detectCategory } from "../memory.ts"
+export function registerStubCommands(api: OpenClawPluginApi): void {
+ api.registerCommand({
+ name: "remember",
+ description: "Save something to memory",
+ acceptsArgs: true,
+ requireAuth: true,
+ handler: async () => {
+ return {
+ text: "Supermemory not configured. Run 'openclaw supermemory setup' first.",
+ }
+ },
+ })
+
+ api.registerCommand({
+ name: "recall",
+ description: "Search your memories",
+ acceptsArgs: true,
+ requireAuth: true,
+ handler: async () => {
+ return {
+ text: "Supermemory not configured. Run 'openclaw supermemory setup' first.",
+ }
+ },
+ })
+}
+
export function registerCommands(
api: OpenClawPluginApi,
client: SupermemoryClient,
@@ -55,7 +81,7 @@ export function registerCommands(
log.debug(`/recall command: "${query}"`)
try {
- const results = await client.search(query, 5)
+ const results = await client.search(query, _cfg.maxRecallResults)
if (results.length === 0) {
return { text: `No memories found for: "${query}"` }
diff --git a/config.ts b/config.ts
index a687aa4..f592ecb 100644
--- a/config.ts
+++ b/config.ts
@@ -2,8 +2,13 @@ import { hostname } from "node:os"
export type CaptureMode = "everything" | "all"
+export type CustomContainer = {
+ tag: string
+ description: string
+}
+
export type SupermemoryConfig = {
- apiKey: string
+ apiKey: string | undefined
containerTag: string
autoRecall: boolean
autoCapture: boolean
@@ -11,6 +16,9 @@ export type SupermemoryConfig = {
profileFrequency: number
captureMode: CaptureMode
debug: boolean
+ enableCustomContainerTags: boolean
+ customContainers: CustomContainer[]
+ customContainerInstructions: string
}
const ALLOWED_KEYS = [
@@ -22,6 +30,9 @@ const ALLOWED_KEYS = [
"profileFrequency",
"captureMode",
"debug",
+ "enableCustomContainerTags",
+ "customContainers",
+ "customContainerInstructions",
]
function assertAllowedKeys(
@@ -66,15 +77,31 @@ export function parseConfig(raw: unknown): SupermemoryConfig {
assertAllowedKeys(cfg, ALLOWED_KEYS, "supermemory config")
}
- const apiKey =
- typeof cfg.apiKey === "string" && cfg.apiKey.length > 0
- ? resolveEnvVars(cfg.apiKey)
- : process.env.SUPERMEMORY_OPENCLAW_API_KEY
+ let apiKey: string | undefined
+ try {
+ apiKey =
+ typeof cfg.apiKey === "string" && cfg.apiKey.length > 0
+ ? resolveEnvVars(cfg.apiKey)
+ : process.env.SUPERMEMORY_OPENCLAW_API_KEY
+ } catch {
+ apiKey = undefined
+ }
- if (!apiKey) {
- throw new Error(
- "supermemory: apiKey is required (set in plugin config or SUPERMEMORY_OPENCLAW_API_KEY env var)",
- )
+ const customContainers: CustomContainer[] = []
+ if (Array.isArray(cfg.customContainers)) {
+ for (const c of cfg.customContainers) {
+ if (
+ c &&
+ typeof c === "object" &&
+ typeof (c as Record).tag === "string" &&
+ typeof (c as Record).description === "string"
+ ) {
+ customContainers.push({
+ tag: sanitizeTag((c as Record).tag as string),
+ description: (c as Record).description as string,
+ })
+ }
+ }
}
return {
@@ -91,9 +118,43 @@ export function parseConfig(raw: unknown): SupermemoryConfig {
? ("everything" as const)
: ("all" as const),
debug: (cfg.debug as boolean) ?? false,
+ enableCustomContainerTags:
+ (cfg.enableCustomContainerTags as boolean) ?? false,
+ customContainers,
+ customContainerInstructions:
+ typeof cfg.customContainerInstructions === "string"
+ ? cfg.customContainerInstructions
+ : "",
}
}
export const supermemoryConfigSchema = {
+ jsonSchema: {
+ type: "object",
+ additionalProperties: false,
+ properties: {
+ apiKey: { type: "string" },
+ containerTag: { type: "string" },
+ autoRecall: { type: "boolean" },
+ autoCapture: { type: "boolean" },
+ maxRecallResults: { type: "number" },
+ profileFrequency: { type: "number" },
+ captureMode: { type: "string", enum: ["all", "everything"] },
+ debug: { type: "boolean" },
+ enableCustomContainerTags: { type: "boolean" },
+ customContainers: {
+ type: "array",
+ items: {
+ type: "object",
+ properties: {
+ tag: { type: "string" },
+ description: { type: "string" },
+ },
+ required: ["tag", "description"],
+ },
+ },
+ customContainerInstructions: { type: "string" },
+ },
+ },
parse: parseConfig,
}
diff --git a/hooks/capture.ts b/hooks/capture.ts
index befbae2..a0c63dc 100644
--- a/hooks/capture.ts
+++ b/hooks/capture.ts
@@ -71,6 +71,10 @@ export function buildCaptureHandler(
/[\s\S]*?<\/supermemory-context>\s*/g,
"",
)
+ .replace(
+ /[\s\S]*?<\/supermemory-containers>\s*/g,
+ "",
+ )
.trim(),
)
.filter((t) => t.length >= 10)
diff --git a/hooks/recall.ts b/hooks/recall.ts
index 2c172d7..ddd2d4e 100644
--- a/hooks/recall.ts
+++ b/hooks/recall.ts
@@ -128,36 +128,87 @@ function countUserTurns(messages: unknown[]): number {
return count
}
+function formatContainerMetadata(
+ cfg: SupermemoryConfig,
+ messageProvider?: string,
+): string | null {
+ if (!cfg.enableCustomContainerTags || cfg.customContainers.length === 0)
+ return null
+
+ const lines: string[] = []
+
+ lines.push(`Root container: \`${cfg.containerTag}\``)
+ lines.push("")
+ lines.push("Custom memory containers:")
+ for (const c of cfg.customContainers) {
+ lines.push(`- \`${c.tag}\`: ${c.description}`)
+ }
+
+ if (messageProvider) {
+ lines.push("")
+ lines.push(`Current channel: ${messageProvider}`)
+ }
+
+ if (cfg.customContainerInstructions) {
+ lines.push("")
+ lines.push(cfg.customContainerInstructions)
+ }
+
+ lines.push("")
+ lines.push(
+ "Use containerTag parameter to store in a specific container, otherwise stores to root.",
+ )
+
+ return lines.join("\n")
+}
+
export function buildRecallHandler(
client: SupermemoryClient,
cfg: SupermemoryConfig,
) {
- return async (event: Record) => {
+ return async (
+ event: Record,
+ ctx?: Record,
+ ) => {
const prompt = event.prompt as string | undefined
if (!prompt || prompt.length < 5) return
const messages = Array.isArray(event.messages) ? event.messages : []
const turn = countUserTurns(messages)
const includeProfile = turn <= 1 || turn % cfg.profileFrequency === 0
+ const messageProvider = ctx?.messageProvider as string | undefined
log.debug(`recalling for turn ${turn} (profile: ${includeProfile})`)
try {
const profile = await client.getProfile(prompt)
- const context = formatContext(
+ const memoryContext = formatContext(
includeProfile ? profile.static : [],
includeProfile ? profile.dynamic : [],
profile.searchResults,
cfg.maxRecallResults,
)
- if (!context) {
+ const containerContext = formatContainerMetadata(cfg, messageProvider)
+
+ const contextParts: string[] = []
+ if (memoryContext) contextParts.push(memoryContext)
+ if (containerContext) {
+ contextParts.push(
+ `\n${containerContext}\n`,
+ )
+ }
+
+ if (contextParts.length === 0) {
log.debug("no profile data to inject")
return
}
- log.debug(`injecting context (${context.length} chars, turn ${turn})`)
- return { prependContext: context }
+ const finalContext = contextParts.join("\n\n")
+ log.debug(
+ `injecting context (${finalContext.length} chars, turn ${turn})`,
+ )
+ return { prependContext: finalContext }
} catch (err) {
log.error("recall failed", err)
return
diff --git a/index.ts b/index.ts
index 4b261b3..9634d45 100644
--- a/index.ts
+++ b/index.ts
@@ -1,7 +1,7 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
import { SupermemoryClient } from "./client.ts"
-import { registerCli } from "./commands/cli.ts"
-import { registerCommands } from "./commands/slash.ts"
+import { registerCli, registerCliSetup } from "./commands/cli.ts"
+import { registerCommands, registerStubCommands } from "./commands/slash.ts"
import { parseConfig, supermemoryConfigSchema } from "./config.ts"
import { buildCaptureHandler } from "./hooks/capture.ts"
import { buildRecallHandler } from "./hooks/recall.ts"
@@ -23,6 +23,16 @@ export default {
initLogger(api.logger, cfg.debug)
+ registerCliSetup(api)
+
+ if (!cfg.apiKey) {
+ api.logger.info(
+ "supermemory: not configured - run 'openclaw supermemory setup'",
+ )
+ registerStubCommands(api)
+ return
+ }
+
const client = new SupermemoryClient(cfg.apiKey, cfg.containerTag)
let sessionKey: string | undefined
@@ -39,7 +49,7 @@ export default {
"before_agent_start",
(event: Record, ctx: Record) => {
if (ctx.sessionKey) sessionKey = ctx.sessionKey as string
- return recallHandler(event)
+ return recallHandler(event, ctx)
},
)
}
diff --git a/openclaw.plugin.json b/openclaw.plugin.json
index 1ce5a54..df11b93 100644
--- a/openclaw.plugin.json
+++ b/openclaw.plugin.json
@@ -43,6 +43,21 @@
"label": "Debug Logging",
"help": "Enable verbose debug logs for API calls and responses",
"advanced": true
+ },
+ "enableCustomContainerTags": {
+ "label": "Enable Custom Container Tags",
+ "help": "Enable AI-driven routing to custom containers",
+ "advanced": true
+ },
+ "customContainers": {
+ "label": "Custom Containers",
+ "help": "Define custom containers with tags and descriptions for AI routing",
+ "advanced": true
+ },
+ "customContainerInstructions": {
+ "label": "Container Instructions",
+ "help": "Instructions for AI on how to route memories to custom containers",
+ "advanced": true
}
},
"configSchema": {
@@ -56,7 +71,20 @@
"maxRecallResults": { "type": "number", "minimum": 1, "maximum": 20 },
"profileFrequency": { "type": "number", "minimum": 1, "maximum": 500 },
"captureMode": { "type": "string", "enum": ["everything", "all"] },
- "debug": { "type": "boolean" }
+ "debug": { "type": "boolean" },
+ "enableCustomContainerTags": { "type": "boolean" },
+ "customContainers": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "tag": { "type": "string" },
+ "description": { "type": "string" }
+ },
+ "required": ["tag", "description"]
+ }
+ },
+ "customContainerInstructions": { "type": "string" }
},
"required": []
}
diff --git a/tools/forget.ts b/tools/forget.ts
index 342838b..6225bc1 100644
--- a/tools/forget.ts
+++ b/tools/forget.ts
@@ -22,22 +22,35 @@ export function registerForgetTool(
memoryId: Type.Optional(
Type.String({ description: "Direct memory ID to delete" }),
),
+ containerTag: Type.Optional(
+ Type.String({
+ description:
+ "Optional container tag to delete from a specific container",
+ }),
+ ),
}),
async execute(
_toolCallId: string,
- params: { query?: string; memoryId?: string },
+ params: { query?: string; memoryId?: string; containerTag?: string },
) {
if (params.memoryId) {
- log.debug(`forget tool: direct delete id="${params.memoryId}"`)
- await client.deleteMemory(params.memoryId)
+ log.debug(
+ `forget tool: direct delete id="${params.memoryId}" containerTag="${params.containerTag ?? "default"}"`,
+ )
+ await client.deleteMemory(params.memoryId, params.containerTag)
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)
+ log.debug(
+ `forget tool: search-then-delete query="${params.query}" containerTag="${params.containerTag ?? "default"}"`,
+ )
+ const result = await client.forgetByQuery(
+ params.query,
+ params.containerTag,
+ )
return {
content: [{ type: "text" as const, text: result.message }],
}
diff --git a/tools/profile.ts b/tools/profile.ts
index d3d626c..8d5f00d 100644
--- a/tools/profile.ts
+++ b/tools/profile.ts
@@ -21,11 +21,25 @@ export function registerProfileTool(
description: "Optional query to focus the profile",
}),
),
+ containerTag: Type.Optional(
+ Type.String({
+ description:
+ "Optional container tag to get profile from a specific container",
+ }),
+ ),
}),
- async execute(_toolCallId: string, params: { query?: string }) {
- log.debug(`profile tool: query="${params.query ?? "(none)"}"`)
+ async execute(
+ _toolCallId: string,
+ params: { query?: string; containerTag?: string },
+ ) {
+ log.debug(
+ `profile tool: query="${params.query ?? "(none)"}" containerTag="${params.containerTag ?? "default"}"`,
+ )
- const profile = await client.getProfile(params.query)
+ const profile = await client.getProfile(
+ params.query,
+ params.containerTag,
+ )
if (profile.static.length === 0 && profile.dynamic.length === 0) {
return {
diff --git a/tools/search.ts b/tools/search.ts
index 41b326e..5cdfce3 100644
--- a/tools/search.ts
+++ b/tools/search.ts
@@ -20,15 +20,27 @@ export function registerSearchTool(
limit: Type.Optional(
Type.Number({ description: "Max results (default: 5)" }),
),
+ containerTag: Type.Optional(
+ Type.String({
+ description:
+ "Optional container tag to search in a specific container",
+ }),
+ ),
}),
async execute(
_toolCallId: string,
- params: { query: string; limit?: number },
+ params: { query: string; limit?: number; containerTag?: string },
) {
const limit = params.limit ?? 5
- log.debug(`search tool: query="${params.query}" limit=${limit}`)
+ log.debug(
+ `search tool: query="${params.query}" limit=${limit} containerTag="${params.containerTag ?? "default"}"`,
+ )
- const results = await client.search(params.query, limit)
+ const results = await client.search(
+ params.query,
+ limit,
+ params.containerTag,
+ )
if (results.length === 0) {
return {
diff --git a/tools/store.ts b/tools/store.ts
index 4ca2aa4..5cfaf02 100644
--- a/tools/store.ts
+++ b/tools/store.ts
@@ -24,21 +24,30 @@ export function registerStoreTool(
parameters: Type.Object({
text: Type.String({ description: "Information to remember" }),
category: Type.Optional(stringEnum(MEMORY_CATEGORIES)),
+ containerTag: Type.Optional(
+ Type.String({
+ description:
+ "Optional container tag to store the memory in a specific container",
+ }),
+ ),
}),
async execute(
_toolCallId: string,
- params: { text: string; category?: string },
+ params: { text: string; category?: string; containerTag?: string },
) {
const category = params.category ?? detectCategory(params.text)
const sk = getSessionKey()
const customId = sk ? buildDocumentId(sk) : undefined
- log.debug(`store tool: category="${category}" customId="${customId}"`)
+ log.debug(
+ `store tool: category="${category}" customId="${customId}" containerTag="${params.containerTag ?? "default"}"`,
+ )
await client.addMemory(
params.text,
{ type: category, source: "openclaw_tool" },
customId,
+ params.containerTag,
)
const preview =