@xmemo/openclaw-memory 1.0.0
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/README.md +212 -0
- package/assets/icon.png +0 -0
- package/dist/doctor-contract-api.d.ts +13 -0
- package/dist/doctor-contract-api.js +60 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +32 -0
- package/dist/openclaw.plugin.json +203 -0
- package/dist/src/auto-capture.d.ts +2 -0
- package/dist/src/auto-capture.js +267 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +73 -0
- package/dist/src/client.d.ts +253 -0
- package/dist/src/client.js +257 -0
- package/dist/src/config.d.ts +30 -0
- package/dist/src/config.js +108 -0
- package/dist/src/memory-text.d.ts +1 -0
- package/dist/src/memory-text.js +12 -0
- package/dist/src/prompt-section.d.ts +2 -0
- package/dist/src/prompt-section.js +31 -0
- package/dist/src/runtime.d.ts +2 -0
- package/dist/src/runtime.js +41 -0
- package/dist/src/search-manager.d.ts +31 -0
- package/dist/src/search-manager.js +153 -0
- package/dist/src/tools.d.ts +2 -0
- package/dist/src/tools.js +814 -0
- package/npm-shrinkwrap.json +21 -0
- package/openclaw.plugin.json +203 -0
- package/package.json +79 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// Thin XMemo REST client. All memory operations are remote HTTP calls.
|
|
2
|
+
// No local vector store or embedding model is required.
|
|
3
|
+
function redactErrorMessage(message, apiKey) {
|
|
4
|
+
if (!apiKey) {
|
|
5
|
+
return message;
|
|
6
|
+
}
|
|
7
|
+
// Replace the literal key so it is never echoed in logs, CLI output, or tool results.
|
|
8
|
+
return message.replaceAll(apiKey, "***");
|
|
9
|
+
}
|
|
10
|
+
/** Structured HTTP error from the XMemo REST client. */
|
|
11
|
+
export class XMemoClientError extends Error {
|
|
12
|
+
status;
|
|
13
|
+
pathname;
|
|
14
|
+
constructor(message, status, pathname) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.status = status;
|
|
17
|
+
this.pathname = pathname;
|
|
18
|
+
this.name = "XMemoClientError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export class XMemoClient {
|
|
22
|
+
baseUrl;
|
|
23
|
+
apiKey;
|
|
24
|
+
agentId;
|
|
25
|
+
agentInstanceId;
|
|
26
|
+
authMode;
|
|
27
|
+
constructor(baseUrl, apiKey, agentId, agentInstanceId, authMode = "api-key") {
|
|
28
|
+
this.baseUrl = baseUrl;
|
|
29
|
+
this.apiKey = apiKey;
|
|
30
|
+
this.agentId = agentId;
|
|
31
|
+
this.agentInstanceId = agentInstanceId;
|
|
32
|
+
this.authMode = authMode;
|
|
33
|
+
}
|
|
34
|
+
isConfigured() {
|
|
35
|
+
return Boolean(this.apiKey);
|
|
36
|
+
}
|
|
37
|
+
headers() {
|
|
38
|
+
const headers = {
|
|
39
|
+
"Content-Type": "application/json",
|
|
40
|
+
"X-Memory-OS-Agent-ID": this.agentId,
|
|
41
|
+
"X-Memory-OS-Agent-Instance-ID": this.agentInstanceId,
|
|
42
|
+
};
|
|
43
|
+
if (this.authMode === "api-key" || this.authMode === "both") {
|
|
44
|
+
headers["X-API-Key"] = this.apiKey;
|
|
45
|
+
}
|
|
46
|
+
if (this.authMode === "bearer" || this.authMode === "both") {
|
|
47
|
+
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
48
|
+
}
|
|
49
|
+
return headers;
|
|
50
|
+
}
|
|
51
|
+
async request(pathname, options = {}) {
|
|
52
|
+
const url = `${this.baseUrl}${pathname}`;
|
|
53
|
+
const response = await fetch(url, {
|
|
54
|
+
...options,
|
|
55
|
+
headers: {
|
|
56
|
+
...this.headers(),
|
|
57
|
+
...options.headers,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
const text = await response.text().catch(() => "unknown error");
|
|
62
|
+
const rawMessage = `XMemo ${pathname} failed (${response.status}): ${text}`;
|
|
63
|
+
throw new XMemoClientError(redactErrorMessage(rawMessage, this.apiKey), response.status, pathname);
|
|
64
|
+
}
|
|
65
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
66
|
+
if (contentType.includes("application/json")) {
|
|
67
|
+
return (await response.json());
|
|
68
|
+
}
|
|
69
|
+
return {};
|
|
70
|
+
}
|
|
71
|
+
buildSearchParams(params) {
|
|
72
|
+
const search = new URLSearchParams();
|
|
73
|
+
for (const [key, value] of Object.entries(params)) {
|
|
74
|
+
if (value === undefined || value === null || value === "") {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
search.set(key, String(value));
|
|
78
|
+
}
|
|
79
|
+
const query = search.toString();
|
|
80
|
+
return query ? `?${query}` : "";
|
|
81
|
+
}
|
|
82
|
+
async remember(request, signal) {
|
|
83
|
+
return this.request("/v1/remember", {
|
|
84
|
+
method: "POST",
|
|
85
|
+
body: JSON.stringify(request),
|
|
86
|
+
signal,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
async validateToken(signal) {
|
|
90
|
+
return this.request("/v1/auth/token/validate", {
|
|
91
|
+
method: "GET",
|
|
92
|
+
signal,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
async recallContext(request, signal) {
|
|
96
|
+
return this.request("/v1/recall/context", {
|
|
97
|
+
method: "POST",
|
|
98
|
+
body: JSON.stringify(request),
|
|
99
|
+
signal,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
async searchMemory(request, signal) {
|
|
103
|
+
const query = this.buildSearchParams({
|
|
104
|
+
query: request.query,
|
|
105
|
+
path: request.path,
|
|
106
|
+
bucket: request.bucket,
|
|
107
|
+
scope: request.scope,
|
|
108
|
+
team_id: request.team_id,
|
|
109
|
+
limit: request.max_items,
|
|
110
|
+
threshold: request.threshold,
|
|
111
|
+
});
|
|
112
|
+
return this.request(`/v1/memories/search${query}`, {
|
|
113
|
+
method: "GET",
|
|
114
|
+
signal,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
async getMemory(id, signal) {
|
|
118
|
+
try {
|
|
119
|
+
return await this.request(`/v1/memories/${encodeURIComponent(id)}`, {
|
|
120
|
+
method: "GET",
|
|
121
|
+
signal,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
// Only fall back to search-by-id when the direct GET endpoint is missing or
|
|
126
|
+
// unavailable (404/405). Auth, timeout, and server errors should surface as-is.
|
|
127
|
+
if (!(error instanceof XMemoClientError) || (error.status !== 404 && error.status !== 405)) {
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
const search = await this.searchMemory({
|
|
131
|
+
query: id,
|
|
132
|
+
bucket: undefined,
|
|
133
|
+
scope: null,
|
|
134
|
+
team_id: null,
|
|
135
|
+
max_items: 5,
|
|
136
|
+
}, signal);
|
|
137
|
+
const match = search.results.find((r) => r.id === id);
|
|
138
|
+
if (match) {
|
|
139
|
+
return {
|
|
140
|
+
id: match.id,
|
|
141
|
+
content: match.content,
|
|
142
|
+
path: match.path,
|
|
143
|
+
bucket: match.bucket,
|
|
144
|
+
scope: match.scope,
|
|
145
|
+
memory_type: match.memory_type,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async updateMemory(id, request, signal) {
|
|
152
|
+
return this.request(`/v1/memories/${encodeURIComponent(id)}`, {
|
|
153
|
+
method: "PATCH",
|
|
154
|
+
body: JSON.stringify(request),
|
|
155
|
+
signal,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
async forgetMemory(id, request, signal) {
|
|
159
|
+
return this.request(`/v1/memories/${encodeURIComponent(id)}/forget`, {
|
|
160
|
+
method: "POST",
|
|
161
|
+
body: JSON.stringify(request ?? {}),
|
|
162
|
+
signal,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
async createReminder(request, signal) {
|
|
166
|
+
return this.request("/v1/reminders", {
|
|
167
|
+
method: "POST",
|
|
168
|
+
body: JSON.stringify(request),
|
|
169
|
+
signal,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
async listReminders(params, signal) {
|
|
173
|
+
const query = this.buildSearchParams({
|
|
174
|
+
bucket: params?.bucket,
|
|
175
|
+
scope: params?.scope,
|
|
176
|
+
item_status: params?.item_status,
|
|
177
|
+
});
|
|
178
|
+
return this.request(`/v1/reminders${query}`, {
|
|
179
|
+
method: "GET",
|
|
180
|
+
signal,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
async completeReminder(id, signal) {
|
|
184
|
+
return this.request(`/v1/reminders/${encodeURIComponent(id)}/complete`, {
|
|
185
|
+
method: "POST",
|
|
186
|
+
signal,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
async recordEvent(request, signal) {
|
|
190
|
+
return this.request("/v1/timeline/events", {
|
|
191
|
+
method: "POST",
|
|
192
|
+
body: JSON.stringify(request),
|
|
193
|
+
signal,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
async getTimeline(params, signal) {
|
|
197
|
+
const query = this.buildSearchParams({
|
|
198
|
+
bucket: params?.bucket,
|
|
199
|
+
scope: params?.scope,
|
|
200
|
+
limit: params?.limit,
|
|
201
|
+
});
|
|
202
|
+
return this.request(`/v1/timeline${query}`, {
|
|
203
|
+
method: "GET",
|
|
204
|
+
signal,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
async saveRestartSnapshot(request, signal) {
|
|
208
|
+
return this.request("/v1/restart/snapshot", {
|
|
209
|
+
method: "POST",
|
|
210
|
+
body: JSON.stringify(request),
|
|
211
|
+
signal,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
async restoreRestartSnapshot(request, signal) {
|
|
215
|
+
return this.request("/v1/restart/restore", {
|
|
216
|
+
method: "POST",
|
|
217
|
+
body: JSON.stringify(request ?? {}),
|
|
218
|
+
signal,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
async getLedgerMonthlySummary(params, signal) {
|
|
222
|
+
const query = this.buildSearchParams({
|
|
223
|
+
month: params?.month,
|
|
224
|
+
year: params?.year,
|
|
225
|
+
currency: params?.currency,
|
|
226
|
+
});
|
|
227
|
+
return this.request(`/v1/me/ledger/monthly-summary${query}`, {
|
|
228
|
+
method: "GET",
|
|
229
|
+
signal,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
async getAuditEvents(params, signal) {
|
|
233
|
+
const query = this.buildSearchParams({
|
|
234
|
+
action: params?.action,
|
|
235
|
+
target_id: params?.target_id,
|
|
236
|
+
limit: params?.limit,
|
|
237
|
+
since: params?.since,
|
|
238
|
+
until: params?.until,
|
|
239
|
+
});
|
|
240
|
+
return this.request(`/v1/audit/events${query}`, {
|
|
241
|
+
method: "GET",
|
|
242
|
+
signal,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
async getAuditConsolidation(params, signal) {
|
|
246
|
+
const query = this.buildSearchParams({
|
|
247
|
+
action_type: params?.action_type,
|
|
248
|
+
limit: params?.limit,
|
|
249
|
+
since: params?.since,
|
|
250
|
+
until: params?.until,
|
|
251
|
+
});
|
|
252
|
+
return this.request(`/v1/audit/consolidation${query}`, {
|
|
253
|
+
method: "GET",
|
|
254
|
+
signal,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
|
2
|
+
export type XMemoAuthMode = "api-key" | "bearer" | "both";
|
|
3
|
+
export type XMemoMemoryConfig = {
|
|
4
|
+
baseUrl: string;
|
|
5
|
+
apiKey: string | undefined;
|
|
6
|
+
bucket: string;
|
|
7
|
+
scope: string | undefined;
|
|
8
|
+
teamId: string | undefined;
|
|
9
|
+
agentId: string;
|
|
10
|
+
agentInstanceId: string;
|
|
11
|
+
authMode: XMemoAuthMode;
|
|
12
|
+
autoCapture: boolean;
|
|
13
|
+
captureMaxChars: number;
|
|
14
|
+
customTriggers: string[] | undefined;
|
|
15
|
+
recallMaxChars: number;
|
|
16
|
+
recallMaxItems: number;
|
|
17
|
+
recallMaxTokens: number;
|
|
18
|
+
};
|
|
19
|
+
export declare const DEFAULT_BASE_URL = "https://xmemo.dev";
|
|
20
|
+
export declare const DEFAULT_BUCKET = "openclaw";
|
|
21
|
+
export declare const DEFAULT_AGENT_ID = "openclaw";
|
|
22
|
+
export declare const DEFAULT_AUTH_MODE: XMemoAuthMode;
|
|
23
|
+
export declare const API_KEY_ENV_VARS: string[];
|
|
24
|
+
export declare const BASE_URL_ENV_VARS: string[];
|
|
25
|
+
export declare const AGENT_ID_ENV_VARS: string[];
|
|
26
|
+
export declare const AGENT_INSTANCE_ID_ENV_VARS: string[];
|
|
27
|
+
export declare function resolveXMemoBaseUrl(env?: NodeJS.ProcessEnv): string;
|
|
28
|
+
export declare function resolveXMemoAgentId(env?: NodeJS.ProcessEnv): string;
|
|
29
|
+
export declare function resolveXMemoAgentInstanceId(env?: NodeJS.ProcessEnv): string;
|
|
30
|
+
export declare function resolveXMemoMemoryConfig(cfg: OpenClawConfig, env?: NodeJS.ProcessEnv): XMemoMemoryConfig;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { resolvePluginConfigObject } from "openclaw/plugin-sdk/plugin-config-runtime";
|
|
3
|
+
import { coerceSecretRef, normalizeSecretInputString } from "openclaw/plugin-sdk/secret-input";
|
|
4
|
+
export const DEFAULT_BASE_URL = "https://xmemo.dev";
|
|
5
|
+
export const DEFAULT_BUCKET = "openclaw";
|
|
6
|
+
export const DEFAULT_AGENT_ID = "openclaw";
|
|
7
|
+
export const DEFAULT_AUTH_MODE = "api-key";
|
|
8
|
+
export const API_KEY_ENV_VARS = ["XMEMO_KEY", "MEMORY_OS_API_KEY", "MEMORY_OS_MCP_TOKEN"];
|
|
9
|
+
export const BASE_URL_ENV_VARS = [
|
|
10
|
+
"XMEMO_BASE_URL",
|
|
11
|
+
"XMEMO_URL",
|
|
12
|
+
"MEMORY_OS_BASE_URL",
|
|
13
|
+
"MEMORY_OS_URL",
|
|
14
|
+
];
|
|
15
|
+
export const AGENT_ID_ENV_VARS = ["XMEMO_AGENT_ID", "MEMORY_OS_AGENT_ID"];
|
|
16
|
+
export const AGENT_INSTANCE_ID_ENV_VARS = [
|
|
17
|
+
"XMEMO_AGENT_INSTANCE_ID",
|
|
18
|
+
"MEMORY_OS_AGENT_INSTANCE_ID",
|
|
19
|
+
];
|
|
20
|
+
function firstEnv(env, keys) {
|
|
21
|
+
for (const key of keys) {
|
|
22
|
+
const value = env[key];
|
|
23
|
+
if (value !== undefined && value !== "") {
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
function normalizeBaseUrl(input) {
|
|
30
|
+
if (!input) {
|
|
31
|
+
return DEFAULT_BASE_URL;
|
|
32
|
+
}
|
|
33
|
+
let url = input.trim();
|
|
34
|
+
if (url.endsWith("/")) {
|
|
35
|
+
url = url.slice(0, -1);
|
|
36
|
+
}
|
|
37
|
+
return url;
|
|
38
|
+
}
|
|
39
|
+
function normalizeAuthMode(input) {
|
|
40
|
+
if (input === "api-key" || input === "bearer" || input === "both") {
|
|
41
|
+
return input;
|
|
42
|
+
}
|
|
43
|
+
return DEFAULT_AUTH_MODE;
|
|
44
|
+
}
|
|
45
|
+
function resolveEnvSecretRef(value, env) {
|
|
46
|
+
const ref = coerceSecretRef(value);
|
|
47
|
+
if (ref && ref.source === "env") {
|
|
48
|
+
return firstEnv(env, [ref.id]);
|
|
49
|
+
}
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
function resolveApiKey(pluginConfig, env) {
|
|
53
|
+
// Prefer the modern `apiKey` config key; fall back to deprecated `token`.
|
|
54
|
+
const fromConfigString = normalizeSecretInputString(pluginConfig.apiKey) ??
|
|
55
|
+
normalizeSecretInputString(pluginConfig.token);
|
|
56
|
+
if (fromConfigString) {
|
|
57
|
+
return fromConfigString;
|
|
58
|
+
}
|
|
59
|
+
// Support canonical env SecretRef: { source: "env", provider: "default", id: "XMEMO_KEY" }.
|
|
60
|
+
// We only resolve env refs here; file/exec must not be silently ignored or falsely promised.
|
|
61
|
+
const fromEnvRef = resolveEnvSecretRef(pluginConfig.apiKey, env) ??
|
|
62
|
+
resolveEnvSecretRef(pluginConfig.token, env);
|
|
63
|
+
if (fromEnvRef) {
|
|
64
|
+
return fromEnvRef;
|
|
65
|
+
}
|
|
66
|
+
// Final fallback to well-known env vars.
|
|
67
|
+
return firstEnv(env, API_KEY_ENV_VARS);
|
|
68
|
+
}
|
|
69
|
+
export function resolveXMemoBaseUrl(env = process.env) {
|
|
70
|
+
return normalizeBaseUrl(firstEnv(env, BASE_URL_ENV_VARS));
|
|
71
|
+
}
|
|
72
|
+
export function resolveXMemoAgentId(env = process.env) {
|
|
73
|
+
return firstEnv(env, AGENT_ID_ENV_VARS) ?? DEFAULT_AGENT_ID;
|
|
74
|
+
}
|
|
75
|
+
let moduleInstanceId;
|
|
76
|
+
export function resolveXMemoAgentInstanceId(env = process.env) {
|
|
77
|
+
// Prefer explicit env. OpenClaw plugins should not silently write JSON sidecars;
|
|
78
|
+
// if no stable id is configured, cache a single process-local id so repeated
|
|
79
|
+
// config resolves stay consistent across runtime/tools/cli/hooks.
|
|
80
|
+
const fromEnv = firstEnv(env, AGENT_INSTANCE_ID_ENV_VARS);
|
|
81
|
+
if (fromEnv) {
|
|
82
|
+
return fromEnv;
|
|
83
|
+
}
|
|
84
|
+
moduleInstanceId ??= `xmemo-${randomUUID()}`;
|
|
85
|
+
return moduleInstanceId;
|
|
86
|
+
}
|
|
87
|
+
export function resolveXMemoMemoryConfig(cfg, env = process.env) {
|
|
88
|
+
// Plugin config lives at plugins.entries["xmemo-memory"].config in resolved OpenClaw config.
|
|
89
|
+
const pluginConfig = resolvePluginConfigObject(cfg, "xmemo-memory") ?? {};
|
|
90
|
+
return {
|
|
91
|
+
baseUrl: normalizeBaseUrl(pluginConfig.baseUrl ?? resolveXMemoBaseUrl(env)),
|
|
92
|
+
apiKey: resolveApiKey(pluginConfig, env),
|
|
93
|
+
bucket: pluginConfig.bucket ?? DEFAULT_BUCKET,
|
|
94
|
+
scope: pluginConfig.scope ?? undefined,
|
|
95
|
+
teamId: pluginConfig.teamId ?? undefined,
|
|
96
|
+
agentId: pluginConfig.agentId ?? resolveXMemoAgentId(env),
|
|
97
|
+
agentInstanceId: resolveXMemoAgentInstanceId(env),
|
|
98
|
+
authMode: normalizeAuthMode(pluginConfig.authMode),
|
|
99
|
+
autoCapture: pluginConfig.autoCapture ?? false,
|
|
100
|
+
captureMaxChars: pluginConfig.captureMaxChars ?? 500,
|
|
101
|
+
customTriggers: Array.isArray(pluginConfig.customTriggers)
|
|
102
|
+
? pluginConfig.customTriggers.filter((s) => typeof s === "string" && s.length > 0)
|
|
103
|
+
: undefined,
|
|
104
|
+
recallMaxChars: pluginConfig.recallMaxChars ?? 1000,
|
|
105
|
+
recallMaxItems: pluginConfig.recallMaxItems ?? 8,
|
|
106
|
+
recallMaxTokens: pluginConfig.recallMaxTokens ?? 1500,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function escapeMemoryForPrompt(text: string): string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Pure text helpers for memory tool output.
|
|
2
|
+
//
|
|
3
|
+
// Kept separate from tools.ts so tests do not pull in plugin-sdk imports that
|
|
4
|
+
// require a full dependency tree (e.g. undici via proxyline).
|
|
5
|
+
export function escapeMemoryForPrompt(text) {
|
|
6
|
+
return text
|
|
7
|
+
.replace(/&/g, "&")
|
|
8
|
+
.replace(/</g, "<")
|
|
9
|
+
.replace(/>/g, ">")
|
|
10
|
+
.replace(/"/g, """)
|
|
11
|
+
.replace(/'/g, "'");
|
|
12
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const buildXMemoPromptSection = ({ availableTools }) => {
|
|
2
|
+
const lines = [];
|
|
3
|
+
lines.push("## XMemo for OpenClaw");
|
|
4
|
+
lines.push("XMemo is enabled as the active long-term memory backend. Relevant project context, decisions, and prior fixes may be injected automatically or retrieved with the memory tools.");
|
|
5
|
+
if (availableTools.has("memory_search")) {
|
|
6
|
+
lines.push("- Use `memory_search` to recall relevant memories before answering questions about prior work.");
|
|
7
|
+
}
|
|
8
|
+
if (availableTools.has("memory_store")) {
|
|
9
|
+
lines.push("- Use `memory_store` to persist durable decisions, conventions, bug fixes, and high-signal context.");
|
|
10
|
+
}
|
|
11
|
+
if (availableTools.has("memory_forget")) {
|
|
12
|
+
lines.push("- Use `memory_forget` to delete a memory by its path/id.");
|
|
13
|
+
}
|
|
14
|
+
if (availableTools.has("xmemo_memory_list")) {
|
|
15
|
+
lines.push("- Use `xmemo_memory_list` to browse recent memories stored in XMemo.");
|
|
16
|
+
}
|
|
17
|
+
if (availableTools.has("xmemo_memory_update")) {
|
|
18
|
+
lines.push("- Use `xmemo_memory_update` to edit an existing memory.");
|
|
19
|
+
}
|
|
20
|
+
if (availableTools.has("xmemo_todo_create")) {
|
|
21
|
+
lines.push("- Use `xmemo_todo_create` to track actionable follow-ups in XMemo.");
|
|
22
|
+
}
|
|
23
|
+
if (availableTools.has("xmemo_record_event")) {
|
|
24
|
+
lines.push("- Use `xmemo_record_event` to record lightweight timeline milestones or decisions.");
|
|
25
|
+
}
|
|
26
|
+
if (availableTools.has("xmemo_restart_snapshot_save")) {
|
|
27
|
+
lines.push("- Use `xmemo_restart_snapshot_save` to checkpoint session state for later recovery.");
|
|
28
|
+
}
|
|
29
|
+
lines.push("- Never store secrets, API keys, tokens, credentials, or sensitive customer data in XMemo.");
|
|
30
|
+
return lines;
|
|
31
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { XMemoClient } from "./client.js";
|
|
2
|
+
import { resolveXMemoMemoryConfig } from "./config.js";
|
|
3
|
+
import { XMemoSearchManager } from "./search-manager.js";
|
|
4
|
+
export function createXMemoMemoryRuntime(_api) {
|
|
5
|
+
return {
|
|
6
|
+
async getMemorySearchManager(params) {
|
|
7
|
+
const cfg = resolveXMemoMemoryConfig(params.cfg);
|
|
8
|
+
if (!cfg.apiKey) {
|
|
9
|
+
return {
|
|
10
|
+
manager: null,
|
|
11
|
+
error: "XMemo is not configured. Set XMEMO_KEY or configure the plugin apiKey.",
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
const client = new XMemoClient(cfg.baseUrl, cfg.apiKey ?? "", cfg.agentId, cfg.agentInstanceId, cfg.authMode);
|
|
16
|
+
const manager = new XMemoSearchManager(client, cfg);
|
|
17
|
+
return { manager };
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
21
|
+
return { manager: null, error: `XMemo memory runtime failed: ${message}` };
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
resolveMemoryBackendConfig(params) {
|
|
25
|
+
const cfg = resolveXMemoMemoryConfig(params.cfg);
|
|
26
|
+
void cfg;
|
|
27
|
+
// OpenClaw core currently only recognizes "builtin" and "qmd" memory backends.
|
|
28
|
+
// XMemo is a custom remote memory provider; report it as builtin so the slot
|
|
29
|
+
// contract stays satisfied without adding a core type patch.
|
|
30
|
+
return {
|
|
31
|
+
backend: "builtin",
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
async closeMemorySearchManager() {
|
|
35
|
+
// Stateless HTTP client; nothing to close.
|
|
36
|
+
},
|
|
37
|
+
async closeAllMemorySearchManagers() {
|
|
38
|
+
// Stateless HTTP client; nothing to close.
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { MemoryEmbeddingProbeResult, MemoryProviderStatus, MemoryReadResult, MemorySearchManager, MemorySearchResult } from "openclaw/plugin-sdk/memory-core-host-engine-storage";
|
|
2
|
+
import type { XMemoClient } from "./client.js";
|
|
3
|
+
import type { XMemoMemoryConfig } from "./config.js";
|
|
4
|
+
export declare class XMemoSearchManager implements MemorySearchManager {
|
|
5
|
+
private readonly client;
|
|
6
|
+
private readonly config;
|
|
7
|
+
private connected;
|
|
8
|
+
private lastError;
|
|
9
|
+
private lastProbeAtMs;
|
|
10
|
+
constructor(client: XMemoClient, config: XMemoMemoryConfig);
|
|
11
|
+
search(query: string, opts?: {
|
|
12
|
+
maxResults?: number;
|
|
13
|
+
minScore?: number;
|
|
14
|
+
sessionKey?: string;
|
|
15
|
+
signal?: AbortSignal;
|
|
16
|
+
sources?: Array<"memory" | "sessions">;
|
|
17
|
+
}): Promise<MemorySearchResult[]>;
|
|
18
|
+
readFile({ relPath, from, lines, }: {
|
|
19
|
+
relPath: string;
|
|
20
|
+
from?: number;
|
|
21
|
+
lines?: number;
|
|
22
|
+
}, signal?: AbortSignal): Promise<MemoryReadResult>;
|
|
23
|
+
probeConnectivity(signal?: AbortSignal): Promise<boolean>;
|
|
24
|
+
status(): MemoryProviderStatus;
|
|
25
|
+
sync(): Promise<void>;
|
|
26
|
+
getCachedEmbeddingAvailability(): MemoryEmbeddingProbeResult | null;
|
|
27
|
+
probeEmbeddingAvailability(): Promise<MemoryEmbeddingProbeResult>;
|
|
28
|
+
probeVectorStoreAvailability(): Promise<boolean>;
|
|
29
|
+
probeVectorAvailability(): Promise<boolean>;
|
|
30
|
+
close(): Promise<void>;
|
|
31
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
function memoryIdFromPath(relPath) {
|
|
2
|
+
// Accept paths like "bucket/id", "bucket/scope/id", or just "id".
|
|
3
|
+
// XMemo ids may be UUIDs or arbitrary strings; take the final non-empty segment.
|
|
4
|
+
const parts = relPath.split("/").filter(Boolean);
|
|
5
|
+
const last = parts[parts.length - 1];
|
|
6
|
+
return last;
|
|
7
|
+
}
|
|
8
|
+
export class XMemoSearchManager {
|
|
9
|
+
client;
|
|
10
|
+
config;
|
|
11
|
+
connected;
|
|
12
|
+
lastError;
|
|
13
|
+
lastProbeAtMs;
|
|
14
|
+
constructor(client, config) {
|
|
15
|
+
this.client = client;
|
|
16
|
+
this.config = config;
|
|
17
|
+
}
|
|
18
|
+
async search(query, opts = {}) {
|
|
19
|
+
if (!this.client.isConfigured()) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
const response = await this.client.recallContext({
|
|
23
|
+
query: query.slice(0, this.config.recallMaxChars),
|
|
24
|
+
bucket: this.config.bucket,
|
|
25
|
+
scope: this.config.scope ?? null,
|
|
26
|
+
team_id: this.config.teamId ?? null,
|
|
27
|
+
max_items: opts.maxResults ?? this.config.recallMaxItems,
|
|
28
|
+
max_tokens: this.config.recallMaxTokens,
|
|
29
|
+
prefer_working: true,
|
|
30
|
+
}, opts.signal);
|
|
31
|
+
this.connected = true;
|
|
32
|
+
this.lastError = undefined;
|
|
33
|
+
return (response.items ?? []).map((item, index) => {
|
|
34
|
+
const score = item.score ?? Math.max(0.5, 0.95 - index * 0.05);
|
|
35
|
+
// Encode the XMemo id into the path so readFile/forget tools can recover it.
|
|
36
|
+
const path = item.path ? `${item.path}/${item.id}` : `${this.config.bucket}/${item.id}`;
|
|
37
|
+
return {
|
|
38
|
+
path,
|
|
39
|
+
startLine: 1,
|
|
40
|
+
endLine: 1,
|
|
41
|
+
score,
|
|
42
|
+
snippet: item.content ?? item.snippet ?? "",
|
|
43
|
+
source: "memory",
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
async readFile({ relPath, from, lines, }, signal) {
|
|
48
|
+
if (!this.client.isConfigured()) {
|
|
49
|
+
return { text: "", path: relPath, truncated: false, from: 1, lines: 0 };
|
|
50
|
+
}
|
|
51
|
+
const id = memoryIdFromPath(relPath);
|
|
52
|
+
let text;
|
|
53
|
+
let path = relPath;
|
|
54
|
+
if (id) {
|
|
55
|
+
const memory = await this.client.getMemory(id, signal);
|
|
56
|
+
text = memory.content;
|
|
57
|
+
path = memory.path ?? relPath;
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
const response = await this.client.searchMemory({
|
|
61
|
+
query: relPath,
|
|
62
|
+
path: relPath,
|
|
63
|
+
bucket: this.config.bucket,
|
|
64
|
+
scope: this.config.scope ?? null,
|
|
65
|
+
team_id: this.config.teamId ?? null,
|
|
66
|
+
max_items: 10,
|
|
67
|
+
}, signal);
|
|
68
|
+
text = response.results.map((r) => r.content).join("\n\n---\n\n");
|
|
69
|
+
}
|
|
70
|
+
this.connected = true;
|
|
71
|
+
this.lastError = undefined;
|
|
72
|
+
const allLines = text.split("\n");
|
|
73
|
+
const startFrom = Math.max(1, from ?? 1);
|
|
74
|
+
const lineCount = lines ?? allLines.length;
|
|
75
|
+
const sliced = allLines.slice(startFrom - 1, startFrom - 1 + lineCount);
|
|
76
|
+
const resultText = sliced.join("\n");
|
|
77
|
+
return {
|
|
78
|
+
text: resultText,
|
|
79
|
+
path,
|
|
80
|
+
truncated: sliced.length < allLines.length,
|
|
81
|
+
from: startFrom,
|
|
82
|
+
lines: sliced.length,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
async probeConnectivity(signal) {
|
|
86
|
+
if (!this.client.isConfigured()) {
|
|
87
|
+
this.connected = false;
|
|
88
|
+
this.lastError = "not configured";
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
await this.client.validateToken(signal);
|
|
93
|
+
this.connected = true;
|
|
94
|
+
this.lastError = undefined;
|
|
95
|
+
this.lastProbeAtMs = Date.now();
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
this.connected = false;
|
|
100
|
+
this.lastError = error instanceof Error ? error.message : String(error);
|
|
101
|
+
this.lastProbeAtMs = Date.now();
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
status() {
|
|
106
|
+
return {
|
|
107
|
+
backend: "builtin",
|
|
108
|
+
provider: "xmemo-memory",
|
|
109
|
+
custom: {
|
|
110
|
+
baseUrl: this.config.baseUrl,
|
|
111
|
+
bucket: this.config.bucket,
|
|
112
|
+
scope: this.config.scope,
|
|
113
|
+
configured: this.client.isConfigured(),
|
|
114
|
+
connected: this.connected ?? false,
|
|
115
|
+
...(this.lastError ? { lastError: this.lastError } : {}),
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
async sync() {
|
|
120
|
+
// XMemo is remote; there is no local index to sync.
|
|
121
|
+
}
|
|
122
|
+
getCachedEmbeddingAvailability() {
|
|
123
|
+
if (this.connected === undefined) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
ok: this.connected,
|
|
128
|
+
error: this.lastError,
|
|
129
|
+
checked: this.lastProbeAtMs !== undefined,
|
|
130
|
+
cached: true,
|
|
131
|
+
checkedAtMs: this.lastProbeAtMs,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
async probeEmbeddingAvailability() {
|
|
135
|
+
const ok = await this.probeConnectivity();
|
|
136
|
+
return {
|
|
137
|
+
ok,
|
|
138
|
+
error: this.lastError,
|
|
139
|
+
checked: true,
|
|
140
|
+
cached: false,
|
|
141
|
+
checkedAtMs: this.lastProbeAtMs,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
async probeVectorStoreAvailability() {
|
|
145
|
+
return await this.probeConnectivity();
|
|
146
|
+
}
|
|
147
|
+
async probeVectorAvailability() {
|
|
148
|
+
return await this.probeConnectivity();
|
|
149
|
+
}
|
|
150
|
+
async close() {
|
|
151
|
+
// HTTP client is stateless.
|
|
152
|
+
}
|
|
153
|
+
}
|