@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.
- package/.github/workflows/ci.yml +31 -0
- package/README.md +89 -0
- package/biome.json +78 -0
- package/bun.lock +1468 -0
- package/client.ts +205 -0
- package/commands/cli.ts +96 -0
- package/commands/slash.ts +80 -0
- package/config.ts +99 -0
- package/hooks/capture.ts +99 -0
- package/hooks/recall.ts +166 -0
- package/index.ts +64 -0
- package/lib/validate.d.ts +28 -0
- package/lib/validate.js +1 -0
- package/logger.ts +54 -0
- package/memory.ts +25 -0
- package/openclaw.plugin.json +63 -0
- package/package.json +28 -0
- package/tools/forget.ts +58 -0
- package/tools/profile.ts +68 -0
- package/tools/search.ts +70 -0
- package/tools/store.ts +54 -0
- package/tsconfig.json +24 -0
- package/types/openclaw.d.ts +24 -0
package/hooks/recall.ts
ADDED
|
@@ -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>
|
package/lib/validate.js
ADDED
|
@@ -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
|
+
}
|
package/tools/forget.ts
ADDED
|
@@ -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
|
+
}
|
package/tools/profile.ts
ADDED
|
@@ -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
|
+
}
|
package/tools/search.ts
ADDED
|
@@ -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
|
+
}
|