@supermemory/openclaw-supermemory 1.0.2

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.
@@ -0,0 +1,166 @@
1
+ import type { ProfileSearchResult, SupermemoryClient } from "../client.ts"
2
+ import type { SupermemoryConfig } from "../config.ts"
3
+ import { log } from "../logger.ts"
4
+
5
+ function formatRelativeTime(isoTimestamp: string): string {
6
+ try {
7
+ const dt = new Date(isoTimestamp)
8
+ const now = new Date()
9
+ const seconds = (now.getTime() - dt.getTime()) / 1000
10
+ const minutes = seconds / 60
11
+ const hours = seconds / 3600
12
+ const days = seconds / 86400
13
+
14
+ if (minutes < 30) return "just now"
15
+ if (minutes < 60) return `${Math.floor(minutes)}mins ago`
16
+ if (hours < 24) return `${Math.floor(hours)} hrs ago`
17
+ if (days < 7) return `${Math.floor(days)}d ago`
18
+
19
+ const month = dt.toLocaleString("en", { month: "short" })
20
+ if (dt.getFullYear() === now.getFullYear()) {
21
+ return `${dt.getDate()} ${month}`
22
+ }
23
+ return `${dt.getDate()} ${month}, ${dt.getFullYear()}`
24
+ } catch {
25
+ return ""
26
+ }
27
+ }
28
+
29
+ function deduplicateMemories(
30
+ staticFacts: string[],
31
+ dynamicFacts: string[],
32
+ searchResults: ProfileSearchResult[],
33
+ ): {
34
+ static: string[]
35
+ dynamic: string[]
36
+ searchResults: ProfileSearchResult[]
37
+ } {
38
+ const seen = new Set<string>()
39
+
40
+ const uniqueStatic = staticFacts.filter((m) => {
41
+ if (seen.has(m)) return false
42
+ seen.add(m)
43
+ return true
44
+ })
45
+
46
+ const uniqueDynamic = dynamicFacts.filter((m) => {
47
+ if (seen.has(m)) return false
48
+ seen.add(m)
49
+ return true
50
+ })
51
+
52
+ const uniqueSearch = searchResults.filter((r) => {
53
+ const memory = r.memory ?? ""
54
+ if (!memory || seen.has(memory)) return false
55
+ seen.add(memory)
56
+ return true
57
+ })
58
+
59
+ return {
60
+ static: uniqueStatic,
61
+ dynamic: uniqueDynamic,
62
+ searchResults: uniqueSearch,
63
+ }
64
+ }
65
+
66
+ function formatContext(
67
+ staticFacts: string[],
68
+ dynamicFacts: string[],
69
+ searchResults: ProfileSearchResult[],
70
+ maxResults: number,
71
+ ): string | null {
72
+ const deduped = deduplicateMemories(staticFacts, dynamicFacts, searchResults)
73
+ const statics = deduped.static.slice(0, maxResults)
74
+ const dynamics = deduped.dynamic.slice(0, maxResults)
75
+ const search = deduped.searchResults.slice(0, maxResults)
76
+
77
+ if (statics.length === 0 && dynamics.length === 0 && search.length === 0)
78
+ return null
79
+
80
+ const sections: string[] = []
81
+
82
+ if (statics.length > 0) {
83
+ sections.push(
84
+ "## User Profile (Persistent)\n" +
85
+ statics.map((f) => `- ${f}`).join("\n"),
86
+ )
87
+ }
88
+
89
+ if (dynamics.length > 0) {
90
+ sections.push(
91
+ `## Recent Context\n${dynamics.map((f) => `- ${f}`).join("\n")}`,
92
+ )
93
+ }
94
+
95
+ if (search.length > 0) {
96
+ const lines = search.map((r) => {
97
+ const memory = r.memory ?? ""
98
+ const timeStr = r.updatedAt ? formatRelativeTime(r.updatedAt) : ""
99
+ const pct =
100
+ r.similarity != null ? `[${Math.round(r.similarity * 100)}%]` : ""
101
+ const prefix = timeStr ? `[${timeStr}]` : ""
102
+ return `- ${prefix}${memory} ${pct}`.trim()
103
+ })
104
+ sections.push(
105
+ `## Relevant Memories (with relevance %)\n${lines.join("\n")}`,
106
+ )
107
+ }
108
+
109
+ const intro =
110
+ "The following is recalled context about the user. Reference it only when relevant to the conversation."
111
+ const disclaimer =
112
+ "Use these memories naturally when relevant — including indirect connections — but don't force them into every response or make assumptions beyond what's stated."
113
+
114
+ return `<supermemory-context>\n${intro}\n\n${sections.join("\n\n")}\n\n${disclaimer}\n</supermemory-context>`
115
+ }
116
+
117
+ function countUserTurns(messages: unknown[]): number {
118
+ let count = 0
119
+ for (const msg of messages) {
120
+ if (
121
+ msg &&
122
+ typeof msg === "object" &&
123
+ (msg as Record<string, unknown>).role === "user"
124
+ ) {
125
+ count++
126
+ }
127
+ }
128
+ return count
129
+ }
130
+
131
+ export function buildRecallHandler(
132
+ client: SupermemoryClient,
133
+ cfg: SupermemoryConfig,
134
+ ) {
135
+ return async (event: Record<string, unknown>) => {
136
+ const prompt = event.prompt as string | undefined
137
+ if (!prompt || prompt.length < 5) return
138
+
139
+ const messages = Array.isArray(event.messages) ? event.messages : []
140
+ const turn = countUserTurns(messages)
141
+ const includeProfile = turn <= 1 || turn % cfg.profileFrequency === 0
142
+
143
+ log.debug(`recalling for turn ${turn} (profile: ${includeProfile})`)
144
+
145
+ try {
146
+ const profile = await client.getProfile(prompt)
147
+ const context = formatContext(
148
+ includeProfile ? profile.static : [],
149
+ includeProfile ? profile.dynamic : [],
150
+ profile.searchResults,
151
+ cfg.maxRecallResults,
152
+ )
153
+
154
+ if (!context) {
155
+ log.debug("no profile data to inject")
156
+ return
157
+ }
158
+
159
+ log.debug(`injecting context (${context.length} chars, turn ${turn})`)
160
+ return { prependContext: context }
161
+ } catch (err) {
162
+ log.error("recall failed", err)
163
+ return
164
+ }
165
+ }
166
+ }
package/index.ts ADDED
@@ -0,0 +1,64 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
2
+ import { SupermemoryClient } from "./client.ts"
3
+ import { registerCli } from "./commands/cli.ts"
4
+ import { registerCommands } from "./commands/slash.ts"
5
+ import { parseConfig, supermemoryConfigSchema } from "./config.ts"
6
+ import { buildCaptureHandler } from "./hooks/capture.ts"
7
+ import { buildRecallHandler } from "./hooks/recall.ts"
8
+ import { initLogger } from "./logger.ts"
9
+ import { registerForgetTool } from "./tools/forget.ts"
10
+ import { registerProfileTool } from "./tools/profile.ts"
11
+ import { registerSearchTool } from "./tools/search.ts"
12
+ import { registerStoreTool } from "./tools/store.ts"
13
+
14
+ export default {
15
+ id: "openclaw-supermemory",
16
+ name: "Supermemory",
17
+ description: "OpenClaw powered by Supermemory plugin",
18
+ kind: "memory" as const,
19
+ configSchema: supermemoryConfigSchema,
20
+
21
+ register(api: OpenClawPluginApi) {
22
+ const cfg = parseConfig(api.pluginConfig)
23
+
24
+ initLogger(api.logger, cfg.debug)
25
+
26
+ const client = new SupermemoryClient(cfg.apiKey, cfg.containerTag)
27
+
28
+ let sessionKey: string | undefined
29
+ const getSessionKey = () => sessionKey
30
+
31
+ registerSearchTool(api, client, cfg)
32
+ registerStoreTool(api, client, cfg, getSessionKey)
33
+ registerForgetTool(api, client, cfg)
34
+ registerProfileTool(api, client, cfg)
35
+
36
+ if (cfg.autoRecall) {
37
+ const recallHandler = buildRecallHandler(client, cfg)
38
+ api.on(
39
+ "before_agent_start",
40
+ (event: Record<string, unknown>, ctx: Record<string, unknown>) => {
41
+ if (ctx.sessionKey) sessionKey = ctx.sessionKey as string
42
+ return recallHandler(event)
43
+ },
44
+ )
45
+ }
46
+
47
+ if (cfg.autoCapture) {
48
+ api.on("agent_end", buildCaptureHandler(client, cfg, getSessionKey))
49
+ }
50
+
51
+ registerCommands(api, client, cfg, getSessionKey)
52
+ registerCli(api, client, cfg)
53
+
54
+ api.registerService({
55
+ id: "openclaw-supermemory",
56
+ start: () => {
57
+ api.logger.info("supermemory: connected")
58
+ },
59
+ stop: () => {
60
+ api.logger.info("supermemory: stopped")
61
+ },
62
+ })
63
+ },
64
+ }
@@ -0,0 +1,28 @@
1
+ export declare function validateApiKeyFormat(key: string): {
2
+ valid: boolean
3
+ reason?: string
4
+ }
5
+ export declare function validateContainerTag(tag: string): {
6
+ valid: boolean
7
+ reason?: string
8
+ }
9
+ export declare function sanitizeContent(
10
+ content: string,
11
+ maxLength?: number,
12
+ ): string
13
+ export declare function validateContentLength(
14
+ content: string,
15
+ min?: number,
16
+ max?: number,
17
+ ): { valid: boolean; reason?: string }
18
+ export declare function sanitizeMetadata(
19
+ meta: Record<string, unknown>,
20
+ ): Record<string, string | number | boolean>
21
+ export declare function validateRecallConfig(
22
+ maxResults: number,
23
+ frequency: number,
24
+ ): string[]
25
+ export declare function getRequestIntegrity(
26
+ apiKey: string,
27
+ containerTag: string,
28
+ ): Record<string, string>
@@ -0,0 +1 @@
1
+ import{createHash as o,createHmac as l}from"node:crypto";function h(e){return!e||typeof e!="string"?{valid:!1,reason:"key is empty or not a string"}:e.startsWith("sm_")?e.length<20?{valid:!1,reason:"key is too short"}:/\s/.test(e)?{valid:!1,reason:"key contains whitespace"}:{valid:!0}:{valid:!1,reason:"key must start with sm_ prefix"}}function v(e){return!e||typeof e!="string"?{valid:!1,reason:"tag is empty"}:e.length>100?{valid:!1,reason:"tag exceeds 100 characters"}:/^[a-zA-Z0-9_-]+$/.test(e)?/^[-_]|[-_]$/.test(e)?{valid:!1,reason:"tag must not start or end with - or _"}:{valid:!0}:{valid:!1,reason:"tag contains invalid characters (only alphanumeric, underscore, hyphen allowed)"}}var u=[/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g,/\uFEFF/g,/[\uFFF0-\uFFFF]/g];function m(e,n=1e5){if(!e||typeof e!="string")return"";let t=e;for(let r of u)t=t.replace(r,"");return t.length>n&&(t=t.slice(0,n)),t}function A(e,n=1,t=1e5){return e.length<n?{valid:!1,reason:`content below minimum length (${n})`}:e.length>t?{valid:!1,reason:`content exceeds maximum length (${t})`}:{valid:!0}}var f=50,g=128,c=1024;function x(e){let n={},t=0;for(let[r,i]of Object.entries(e)){if(t>=f)break;r.length>g||/[^\w.-]/.test(r)||(typeof i=="string"?(n[r]=i.slice(0,c),t++):(typeof i=="number"&&Number.isFinite(i)||typeof i=="boolean")&&(n[r]=i,t++))}return n}function E(e,n){let t=[];return(!Number.isInteger(e)||e<1||e>20)&&t.push("maxRecallResults must be an integer between 1 and 20"),(!Number.isInteger(n)||n<1||n>500)&&t.push("profileFrequency must be an integer between 1 and 500"),t}function s(e){return o("sha256").update(e).digest("hex")}var a=1,d="7f2a9c4b8e1d6f3a5c0b9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a";function p(e,n){let t=[s(e),s(n),a].join(":");return l("sha256",d).update(t).digest("base64url")}function F(e,n){let t=s(n),r=p(e,n);return{"X-Content-Hash":t,"X-Request-Integrity":[`v${a}`,r].join(".")}}export{F as getRequestIntegrity,m as sanitizeContent,x as sanitizeMetadata,h as validateApiKeyFormat,v as validateContainerTag,A as validateContentLength,E as validateRecallConfig};
package/logger.ts ADDED
@@ -0,0 +1,54 @@
1
+ export type LoggerBackend = {
2
+ info(msg: string, ...args: unknown[]): void
3
+ warn(msg: string, ...args: unknown[]): void
4
+ error(msg: string, ...args: unknown[]): void
5
+ debug?(msg: string, ...args: unknown[]): void
6
+ }
7
+
8
+ const NOOP_LOGGER: LoggerBackend = {
9
+ info() {},
10
+ warn() {},
11
+ error() {},
12
+ debug() {},
13
+ }
14
+
15
+ let _backend: LoggerBackend = NOOP_LOGGER
16
+ let _debug = false
17
+
18
+ export function initLogger(backend: LoggerBackend, debug: boolean): void {
19
+ _backend = backend
20
+ _debug = debug
21
+ }
22
+
23
+ export const log = {
24
+ info(msg: string, ...args: unknown[]): void {
25
+ _backend.info(`supermemory: ${msg}`, ...args)
26
+ },
27
+
28
+ warn(msg: string, ...args: unknown[]): void {
29
+ _backend.warn(`supermemory: ${msg}`, ...args)
30
+ },
31
+
32
+ error(msg: string, err?: unknown): void {
33
+ const detail = err instanceof Error ? err.message : err ? String(err) : ""
34
+ _backend.error(`supermemory: ${msg}${detail ? ` — ${detail}` : ""}`)
35
+ },
36
+
37
+ debug(msg: string, ...args: unknown[]): void {
38
+ if (!_debug) return
39
+ const fn = _backend.debug ?? _backend.info
40
+ fn(`supermemory [debug]: ${msg}`, ...args)
41
+ },
42
+
43
+ debugRequest(method: string, params: Record<string, unknown>): void {
44
+ if (!_debug) return
45
+ const fn = _backend.debug ?? _backend.info
46
+ fn(`supermemory [debug] → ${method}`, JSON.stringify(params, null, 2))
47
+ },
48
+
49
+ debugResponse(method: string, data: unknown): void {
50
+ if (!_debug) return
51
+ const fn = _backend.debug ?? _backend.info
52
+ fn(`supermemory [debug] ← ${method}`, JSON.stringify(data, null, 2))
53
+ },
54
+ }
package/memory.ts ADDED
@@ -0,0 +1,25 @@
1
+ export const MEMORY_CATEGORIES = [
2
+ "preference",
3
+ "fact",
4
+ "decision",
5
+ "entity",
6
+ "other",
7
+ ] as const
8
+ export type MemoryCategory = (typeof MEMORY_CATEGORIES)[number]
9
+
10
+ export function detectCategory(text: string): MemoryCategory {
11
+ const lower = text.toLowerCase()
12
+ if (/prefer|like|love|hate|want/i.test(lower)) return "preference"
13
+ if (/decided|will use|going with/i.test(lower)) return "decision"
14
+ if (/\+\d{10,}|@[\w.-]+\.\w+|is called/i.test(lower)) return "entity"
15
+ if (/is|are|has|have/i.test(lower)) return "fact"
16
+ return "other"
17
+ }
18
+
19
+ export function buildDocumentId(sessionKey: string): string {
20
+ const sanitized = sessionKey
21
+ .replace(/[^a-zA-Z0-9_]/g, "_")
22
+ .replace(/_+/g, "_")
23
+ .replace(/^_|_$/g, "")
24
+ return `session_${sanitized}`
25
+ }
@@ -0,0 +1,63 @@
1
+ {
2
+ "id": "openclaw-supermemory",
3
+ "kind": "memory",
4
+ "uiHints": {
5
+ "apiKey": {
6
+ "label": "Supermemory API Key",
7
+ "sensitive": true,
8
+ "placeholder": "sm_...",
9
+ "help": "Your API key from console.supermemory.ai (or use ${SUPERMEMORY_OPENCLAW_API_KEY})"
10
+ },
11
+ "containerTag": {
12
+ "label": "Container Tag",
13
+ "placeholder": "openclaw_myhostname",
14
+ "help": "Memory namespace. Default: openclaw_{hostname}. All channels share this.",
15
+ "advanced": true
16
+ },
17
+ "autoRecall": {
18
+ "label": "Auto-Recall",
19
+ "help": "Inject relevant memories + user profile before every AI turn"
20
+ },
21
+ "autoCapture": {
22
+ "label": "Auto-Capture",
23
+ "help": "Automatically store important information from conversations"
24
+ },
25
+ "maxRecallResults": {
26
+ "label": "Max Recall Results",
27
+ "placeholder": "10",
28
+ "help": "Maximum memories injected into context per turn",
29
+ "advanced": true
30
+ },
31
+ "profileFrequency": {
32
+ "label": "Profile Injection Frequency",
33
+ "placeholder": "50",
34
+ "help": "Inject full profile (static + dynamic) every N turns. Search results are injected every turn.",
35
+ "advanced": true
36
+ },
37
+ "captureMode": {
38
+ "label": "Capture Mode",
39
+ "help": "'all' (default) = filter out short texts and injected context, 'everything' = capture all messages",
40
+ "advanced": true
41
+ },
42
+ "debug": {
43
+ "label": "Debug Logging",
44
+ "help": "Enable verbose debug logs for API calls and responses",
45
+ "advanced": true
46
+ }
47
+ },
48
+ "configSchema": {
49
+ "type": "object",
50
+ "additionalProperties": false,
51
+ "properties": {
52
+ "apiKey": { "type": "string" },
53
+ "containerTag": { "type": "string" },
54
+ "autoRecall": { "type": "boolean" },
55
+ "autoCapture": { "type": "boolean" },
56
+ "maxRecallResults": { "type": "number", "minimum": 1, "maximum": 20 },
57
+ "profileFrequency": { "type": "number", "minimum": 1, "maximum": 500 },
58
+ "captureMode": { "type": "string", "enum": ["everything", "all"] },
59
+ "debug": { "type": "boolean" }
60
+ },
61
+ "required": []
62
+ }
63
+ }
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@supermemory/openclaw-supermemory",
3
+ "version": "1.0.2",
4
+ "type": "module",
5
+ "description": "OpenClaw Supermemory memory plugin",
6
+ "license": "MIT",
7
+ "dependencies": {
8
+ "supermemory": "^4.0.0",
9
+ "@sinclair/typebox": "0.34.47"
10
+ },
11
+ "scripts": {
12
+ "check-types": "tsc --noEmit",
13
+ "lint": "bunx @biomejs/biome ci .",
14
+ "lint:fix": "bunx @biomejs/biome check --write .",
15
+ "build:lib": "esbuild lib/validate.ts --bundle --minify --format=esm --platform=node --target=es2022 --external:node:crypto --outfile=lib/validate.js"
16
+ },
17
+ "peerDependencies": {
18
+ "openclaw": ">=2026.1.29"
19
+ },
20
+ "openclaw": {
21
+ "extensions": [
22
+ "./index.ts"
23
+ ]
24
+ },
25
+ "devDependencies": {
26
+ "typescript": "^5.9.3"
27
+ }
28
+ }
@@ -0,0 +1,58 @@
1
+ import { Type } from "@sinclair/typebox"
2
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
3
+ import type { SupermemoryClient } from "../client.ts"
4
+ import type { SupermemoryConfig } from "../config.ts"
5
+ import { log } from "../logger.ts"
6
+
7
+ export function registerForgetTool(
8
+ api: OpenClawPluginApi,
9
+ client: SupermemoryClient,
10
+ _cfg: SupermemoryConfig,
11
+ ): void {
12
+ api.registerTool(
13
+ {
14
+ name: "supermemory_forget",
15
+ label: "Memory Forget",
16
+ description:
17
+ "Forget/delete a specific memory. Searches for the closest match and removes it.",
18
+ parameters: Type.Object({
19
+ query: Type.Optional(
20
+ Type.String({ description: "Describe the memory to forget" }),
21
+ ),
22
+ memoryId: Type.Optional(
23
+ Type.String({ description: "Direct memory ID to delete" }),
24
+ ),
25
+ }),
26
+ async execute(
27
+ _toolCallId: string,
28
+ params: { query?: string; memoryId?: string },
29
+ ) {
30
+ if (params.memoryId) {
31
+ log.debug(`forget tool: direct delete id="${params.memoryId}"`)
32
+ await client.deleteMemory(params.memoryId)
33
+ return {
34
+ content: [{ type: "text" as const, text: "Memory forgotten." }],
35
+ }
36
+ }
37
+
38
+ if (params.query) {
39
+ log.debug(`forget tool: search-then-delete query="${params.query}"`)
40
+ const result = await client.forgetByQuery(params.query)
41
+ return {
42
+ content: [{ type: "text" as const, text: result.message }],
43
+ }
44
+ }
45
+
46
+ return {
47
+ content: [
48
+ {
49
+ type: "text" as const,
50
+ text: "Provide a query or memoryId to forget.",
51
+ },
52
+ ],
53
+ }
54
+ },
55
+ },
56
+ { name: "supermemory_forget" },
57
+ )
58
+ }
@@ -0,0 +1,68 @@
1
+ import { Type } from "@sinclair/typebox"
2
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
3
+ import type { SupermemoryClient } from "../client.ts"
4
+ import type { SupermemoryConfig } from "../config.ts"
5
+ import { log } from "../logger.ts"
6
+
7
+ export function registerProfileTool(
8
+ api: OpenClawPluginApi,
9
+ client: SupermemoryClient,
10
+ _cfg: SupermemoryConfig,
11
+ ): void {
12
+ api.registerTool(
13
+ {
14
+ name: "supermemory_profile",
15
+ label: "User Profile",
16
+ description:
17
+ "Get a summary of what is known about the user — stable preferences and recent context.",
18
+ parameters: Type.Object({
19
+ query: Type.Optional(
20
+ Type.String({
21
+ description: "Optional query to focus the profile",
22
+ }),
23
+ ),
24
+ }),
25
+ async execute(_toolCallId: string, params: { query?: string }) {
26
+ log.debug(`profile tool: query="${params.query ?? "(none)"}"`)
27
+
28
+ const profile = await client.getProfile(params.query)
29
+
30
+ if (profile.static.length === 0 && profile.dynamic.length === 0) {
31
+ return {
32
+ content: [
33
+ {
34
+ type: "text" as const,
35
+ text: "No profile information available yet.",
36
+ },
37
+ ],
38
+ }
39
+ }
40
+
41
+ const sections: string[] = []
42
+
43
+ if (profile.static.length > 0) {
44
+ sections.push(
45
+ "## User Profile (Persistent)\n" +
46
+ profile.static.map((f) => `- ${f}`).join("\n"),
47
+ )
48
+ }
49
+
50
+ if (profile.dynamic.length > 0) {
51
+ sections.push(
52
+ "## Recent Context\n" +
53
+ profile.dynamic.map((f) => `- ${f}`).join("\n"),
54
+ )
55
+ }
56
+
57
+ return {
58
+ content: [{ type: "text" as const, text: sections.join("\n\n") }],
59
+ details: {
60
+ staticCount: profile.static.length,
61
+ dynamicCount: profile.dynamic.length,
62
+ },
63
+ }
64
+ },
65
+ },
66
+ { name: "supermemory_profile" },
67
+ )
68
+ }
@@ -0,0 +1,70 @@
1
+ import { Type } from "@sinclair/typebox"
2
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
3
+ import type { SupermemoryClient } from "../client.ts"
4
+ import type { SupermemoryConfig } from "../config.ts"
5
+ import { log } from "../logger.ts"
6
+
7
+ export function registerSearchTool(
8
+ api: OpenClawPluginApi,
9
+ client: SupermemoryClient,
10
+ _cfg: SupermemoryConfig,
11
+ ): void {
12
+ api.registerTool(
13
+ {
14
+ name: "supermemory_search",
15
+ label: "Memory Search",
16
+ description:
17
+ "Search through long-term memories for relevant information.",
18
+ parameters: Type.Object({
19
+ query: Type.String({ description: "Search query" }),
20
+ limit: Type.Optional(
21
+ Type.Number({ description: "Max results (default: 5)" }),
22
+ ),
23
+ }),
24
+ async execute(
25
+ _toolCallId: string,
26
+ params: { query: string; limit?: number },
27
+ ) {
28
+ const limit = params.limit ?? 5
29
+ log.debug(`search tool: query="${params.query}" limit=${limit}`)
30
+
31
+ const results = await client.search(params.query, limit)
32
+
33
+ if (results.length === 0) {
34
+ return {
35
+ content: [
36
+ { type: "text" as const, text: "No relevant memories found." },
37
+ ],
38
+ }
39
+ }
40
+
41
+ const text = results
42
+ .map((r, i) => {
43
+ const score = r.similarity
44
+ ? ` (${(r.similarity * 100).toFixed(0)}%)`
45
+ : ""
46
+ return `${i + 1}. ${r.content || r.memory || ""}${score}`
47
+ })
48
+ .join("\n")
49
+
50
+ return {
51
+ content: [
52
+ {
53
+ type: "text" as const,
54
+ text: `Found ${results.length} memories:\n\n${text}`,
55
+ },
56
+ ],
57
+ details: {
58
+ count: results.length,
59
+ memories: results.map((r) => ({
60
+ id: r.id,
61
+ content: r.content,
62
+ similarity: r.similarity,
63
+ })),
64
+ },
65
+ }
66
+ },
67
+ },
68
+ { name: "supermemory_search" },
69
+ )
70
+ }
package/tools/store.ts ADDED
@@ -0,0 +1,54 @@
1
+ import { Type } from "@sinclair/typebox"
2
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
3
+ import { stringEnum } from "openclaw/plugin-sdk"
4
+ import type { SupermemoryClient } from "../client.ts"
5
+ import type { SupermemoryConfig } from "../config.ts"
6
+ import { log } from "../logger.ts"
7
+ import {
8
+ buildDocumentId,
9
+ detectCategory,
10
+ MEMORY_CATEGORIES,
11
+ } from "../memory.ts"
12
+
13
+ export function registerStoreTool(
14
+ api: OpenClawPluginApi,
15
+ client: SupermemoryClient,
16
+ _cfg: SupermemoryConfig,
17
+ getSessionKey: () => string | undefined,
18
+ ): void {
19
+ api.registerTool(
20
+ {
21
+ name: "supermemory_store",
22
+ label: "Memory Store",
23
+ description: "Save important information to long-term memory.",
24
+ parameters: Type.Object({
25
+ text: Type.String({ description: "Information to remember" }),
26
+ category: Type.Optional(stringEnum(MEMORY_CATEGORIES)),
27
+ }),
28
+ async execute(
29
+ _toolCallId: string,
30
+ params: { text: string; category?: string },
31
+ ) {
32
+ const category = params.category ?? detectCategory(params.text)
33
+ const sk = getSessionKey()
34
+ const customId = sk ? buildDocumentId(sk) : undefined
35
+
36
+ log.debug(`store tool: category="${category}" customId="${customId}"`)
37
+
38
+ await client.addMemory(
39
+ params.text,
40
+ { type: category, source: "openclaw_tool" },
41
+ customId,
42
+ )
43
+
44
+ const preview =
45
+ params.text.length > 80 ? `${params.text.slice(0, 80)}…` : params.text
46
+
47
+ return {
48
+ content: [{ type: "text" as const, text: `Stored: "${preview}"` }],
49
+ }
50
+ },
51
+ },
52
+ { name: "supermemory_store" },
53
+ )
54
+ }