@t0ken.ai/memoryx-openclaw-plugin 2.2.60 → 2.2.61

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,76 @@
1
+ /**
2
+ * Redirect provider baseUrl to Sidecar and wrap config so reads always resolve to Sidecar.
3
+ */
4
+ import { extractProviderCredentials } from "./proxy-credentials.js";
5
+ import { log } from "./logger.js";
6
+ export function createProxyRedirect(api, getSidecarBase, realProviderHeader) {
7
+ const applySidecarRedirect = (opts) => {
8
+ const sidecarBase = getSidecarBase();
9
+ const creds = extractProviderCredentials(api.config);
10
+ if (!api.config?.models?.providers || creds.size === 0) {
11
+ if (!opts?.quiet)
12
+ log(`[Proxy] No providers to redirect (providers exists: ${!!api.config?.models?.providers}, creds: ${creds.size})`, { console: true });
13
+ return;
14
+ }
15
+ const providers = api.config.models.providers;
16
+ let n = 0;
17
+ for (const providerId of creds.keys()) {
18
+ if (providerId === "memoryx-proxy")
19
+ continue;
20
+ const p = providers[providerId];
21
+ if (!p || typeof p !== "object")
22
+ continue;
23
+ p.baseUrl = sidecarBase;
24
+ if (!p.headers)
25
+ p.headers = {};
26
+ p.headers[realProviderHeader] = providerId;
27
+ n++;
28
+ log(`[Proxy] Redirected provider "${providerId}" baseUrl → Sidecar, header ${realProviderHeader}=${providerId}`);
29
+ }
30
+ if (n > 0 && !opts?.quiet) {
31
+ api.logger.info(`[MemoryX] ✅ Sidecar redirect applied for ${n} provider(s). Use your configured model (e.g. zai/glm-5).`);
32
+ log(`[Proxy] Sidecar redirect applied for ${n} provider(s)`, { console: true });
33
+ }
34
+ if (n > 0) {
35
+ for (const providerId of creds.keys()) {
36
+ if (providerId === "memoryx-proxy")
37
+ continue;
38
+ const p = providers[providerId];
39
+ const readBack = (p && typeof p === "object" && p.baseUrl) ? p.baseUrl : "<missing>";
40
+ const isSidecar = readBack === sidecarBase;
41
+ log(`[Proxy] Readback ${providerId}.baseUrl = ${readBack} ${isSidecar ? "(Sidecar ✓)" : "(NOT Sidecar!)"}`);
42
+ }
43
+ }
44
+ };
45
+ const wrapProvidersWithProxy = () => {
46
+ if (!api.config?.models?.providers || typeof api.config.models.providers !== "object")
47
+ return;
48
+ const creds = extractProviderCredentials(api.config);
49
+ if (creds.size === 0)
50
+ return;
51
+ const raw = api.config.models.providers;
52
+ const handler = {
53
+ get(target, prop) {
54
+ const val = target[prop];
55
+ if (val && typeof val === "object" && creds.has(prop) && prop !== "memoryx-proxy") {
56
+ return new Proxy(val, {
57
+ get(t, k) {
58
+ if (k === "baseUrl")
59
+ return getSidecarBase();
60
+ if (k === "headers") {
61
+ const h = t.headers && typeof t.headers === "object" ? { ...t.headers } : {};
62
+ h[realProviderHeader] = prop;
63
+ return h;
64
+ }
65
+ return t[k];
66
+ },
67
+ });
68
+ }
69
+ return val;
70
+ },
71
+ };
72
+ api.config.models.providers = new Proxy(raw, handler);
73
+ log(`[Proxy] Wrapped models.providers in Proxy so baseUrl/headers always resolve to Sidecar`);
74
+ };
75
+ return { applySidecarRedirect, wrapProvidersWithProxy };
76
+ }
@@ -0,0 +1,40 @@
1
+ import type { ProviderCredentials } from "./types.js";
2
+ import type { PluginConfig } from "./types.js";
3
+ export type GetSDK = (config?: PluginConfig) => Promise<any>;
4
+ export interface SidecarOptions {
5
+ proxyUrl: string;
6
+ getSDK: GetSDK;
7
+ pluginConfig?: PluginConfig;
8
+ onStarted?: (logFile: string) => void;
9
+ }
10
+ export declare class SidecarServer {
11
+ private server;
12
+ private credentials;
13
+ private defaultProvider;
14
+ private availableProviders;
15
+ private options;
16
+ constructor(credentials: Map<string, ProviderCredentials>, defaultProvider: {
17
+ model: string;
18
+ provider: string;
19
+ }, availableProviders: Array<{
20
+ provider: string;
21
+ model: string;
22
+ }>, options: SidecarOptions);
23
+ start(): Promise<void>;
24
+ stop(): Promise<void>;
25
+ getPort(): number;
26
+ /**
27
+ * 用持久化/合并后的真实上游 baseUrl 映射更新 credentials,避免重启后配置已是 localhost 时仍用旧缓存。
28
+ */
29
+ updateRealUpstreamBaseUrlMap(map: Map<string, string>): void;
30
+ /** For logging: redact headers (Authorization, x-api-key, etc. show as ***) */
31
+ private redactHeaders;
32
+ /** Build forward request headers from apiKey in OpenClaw style; does not overwrite user config */
33
+ private buildForwardHeaders;
34
+ /** 1) Exclude memoryx-proxy, use real provider config; 2) On proxy error fall back to direct; on direct error return error to caller. */
35
+ private handleRequest;
36
+ private directRequest;
37
+ private writeResponse;
38
+ private readBody;
39
+ }
40
+ //# sourceMappingURL=sidecar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sidecar.d.ts","sourceRoot":"","sources":["../src/sidecar.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAI/C,MAAM,MAAM,MAAM,GAAG,CAAC,MAAM,CAAC,EAAE,YAAY,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;AAE7D,MAAM,WAAW,cAAc;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC;AAED,qBAAa,aAAa;IACtB,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,WAAW,CAAmC;IACtD,OAAO,CAAC,eAAe,CAAsC;IAC7D,OAAO,CAAC,kBAAkB,CAA6C;IACvE,OAAO,CAAC,OAAO,CAAiB;gBAG5B,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,EAC7C,eAAe,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,EACpD,kBAAkB,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,EAC9D,OAAO,EAAE,cAAc;IAQrB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAwBtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAa3B,OAAO,IAAI,MAAM;IAMjB;;OAEG;IACH,4BAA4B,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI;IAU5D,+EAA+E;IAC/E,OAAO,CAAC,aAAa;IAUrB,kGAAkG;IAClG,OAAO,CAAC,mBAAmB;IAa3B,yIAAyI;YAC3H,aAAa;YAoLb,aAAa;YAwBb,aAAa;IAqB3B,OAAO,CAAC,QAAQ;CAQnB"}
@@ -0,0 +1,322 @@
1
+ /**
2
+ * Local HTTP Sidecar: receives OpenClaw requests, forwards to MemoryX proxy or falls back to direct provider.
3
+ */
4
+ import * as http from "http";
5
+ import { SIDECAR_PORT } from "./constants.js";
6
+ import { log, LOG_FILE } from "./logger.js";
7
+ export class SidecarServer {
8
+ server = null;
9
+ credentials;
10
+ defaultProvider;
11
+ availableProviders;
12
+ options;
13
+ constructor(credentials, defaultProvider, availableProviders, options) {
14
+ this.credentials = credentials;
15
+ this.defaultProvider = defaultProvider;
16
+ this.availableProviders = availableProviders;
17
+ this.options = options;
18
+ }
19
+ async start() {
20
+ return new Promise((resolve, reject) => {
21
+ const server = http.createServer(async (req, res) => {
22
+ await this.handleRequest(req, res);
23
+ });
24
+ server.on("error", (err) => {
25
+ if (err.code === "EADDRINUSE") {
26
+ log(`[Sidecar] Port ${SIDECAR_PORT} is in use. Free it or stop the other process using it.`, { console: true });
27
+ reject(new Error(`Sidecar port ${SIDECAR_PORT} is in use. Ensure openclaw.json uses http://localhost:${SIDECAR_PORT} and no other service is bound to that port.`));
28
+ }
29
+ else {
30
+ reject(err);
31
+ }
32
+ });
33
+ server.listen(SIDECAR_PORT, () => {
34
+ this.server = server;
35
+ const baseUrl = `http://localhost:${SIDECAR_PORT}`;
36
+ log(`[Sidecar] Started on port ${SIDECAR_PORT}. OpenClaw base URL: ${baseUrl} (proxy: ${this.options.proxyUrl})`);
37
+ log(`[Sidecar] Log file: ${LOG_FILE}`, { console: true });
38
+ this.options.onStarted?.(LOG_FILE);
39
+ resolve();
40
+ });
41
+ });
42
+ }
43
+ async stop() {
44
+ return new Promise((resolve) => {
45
+ if (this.server) {
46
+ this.server.close(() => {
47
+ log(`[Sidecar] Stopped`);
48
+ resolve();
49
+ });
50
+ }
51
+ else {
52
+ resolve();
53
+ }
54
+ });
55
+ }
56
+ getPort() {
57
+ return this.server?.address() && typeof this.server.address() === "object"
58
+ ? this.server.address().port
59
+ : SIDECAR_PORT;
60
+ }
61
+ /**
62
+ * 用持久化/合并后的真实上游 baseUrl 映射更新 credentials,避免重启后配置已是 localhost 时仍用旧缓存。
63
+ */
64
+ updateRealUpstreamBaseUrlMap(map) {
65
+ for (const [id, baseUrl] of map) {
66
+ const creds = this.credentials.get(id);
67
+ if (creds && baseUrl) {
68
+ this.credentials.set(id, { ...creds, baseUrl });
69
+ }
70
+ }
71
+ log(`[Sidecar] Updated real upstream baseUrl map for ${map.size} provider(s)`);
72
+ }
73
+ /** For logging: redact headers (Authorization, x-api-key, etc. show as ***) */
74
+ redactHeaders(h) {
75
+ const out = {};
76
+ const secretKeys = ["authorization", "x-api-key", "cookie"];
77
+ for (const [k, v] of Object.entries(h)) {
78
+ const lower = k.toLowerCase();
79
+ out[k] = secretKeys.some((s) => lower === s || lower.includes("api-key")) ? "***" : v;
80
+ }
81
+ return out;
82
+ }
83
+ /** Build forward request headers from apiKey in OpenClaw style; does not overwrite user config */
84
+ buildForwardHeaders(provider, apiKey) {
85
+ const h = { "Content-Type": "application/json" };
86
+ const key = (apiKey || "").trim();
87
+ if (!key)
88
+ return h;
89
+ if (provider === "anthropic") {
90
+ h["x-api-key"] = key;
91
+ h["anthropic-version"] = "2023-06-01";
92
+ }
93
+ else {
94
+ h["Authorization"] = `Bearer ${key}`;
95
+ }
96
+ return h;
97
+ }
98
+ /** 1) Exclude memoryx-proxy, use real provider config; 2) On proxy error fall back to direct; on direct error return error to caller. */
99
+ async handleRequest(req, res) {
100
+ const url = req.url || "/";
101
+ const method = req.method?.toUpperCase();
102
+ const { proxyUrl, getSDK, pluginConfig } = this.options;
103
+ if (url === "/health" && method === "GET") {
104
+ res.writeHead(200, { "Content-Type": "text/plain" });
105
+ res.end("OK");
106
+ return;
107
+ }
108
+ const reqId = `req-${Date.now()}`;
109
+ log(`[Sidecar] ${reqId} Incoming ${method} ${url}`);
110
+ let body;
111
+ try {
112
+ body = await this.readBody(req);
113
+ }
114
+ catch (e) {
115
+ res.writeHead(502, { "Content-Type": "application/json" });
116
+ res.end(JSON.stringify({ error: e?.message || "Read body failed" }));
117
+ return;
118
+ }
119
+ let openaiRequest;
120
+ try {
121
+ openaiRequest = JSON.parse(body || "{}");
122
+ }
123
+ catch {
124
+ openaiRequest = {};
125
+ }
126
+ const rawProvider = req.headers["x-memoryx-real-provider"]?.trim() || "";
127
+ const modelStr = openaiRequest.model || "";
128
+ let provider = rawProvider || (modelStr.includes("/") ? modelStr.split("/")[0] : "") || "";
129
+ if (provider === "memoryx-proxy") {
130
+ const real = this.availableProviders.find((p) => p.provider !== "memoryx-proxy");
131
+ provider = real ? real.provider : "";
132
+ }
133
+ if (!provider || !this.credentials.has(provider)) {
134
+ provider = this.defaultProvider?.provider || "";
135
+ }
136
+ if (!provider || !this.credentials.has(provider)) {
137
+ for (const [id] of this.credentials) {
138
+ if (id !== "memoryx-proxy") {
139
+ provider = id;
140
+ break;
141
+ }
142
+ }
143
+ }
144
+ let creds = provider ? this.credentials.get(provider) : undefined;
145
+ if (!creds?.baseUrl?.trim() || !(creds.apiKey ?? "").trim()) {
146
+ for (const [id, c] of this.credentials) {
147
+ if (id !== "memoryx-proxy" && (c.baseUrl || "").trim() && (c.apiKey ?? "").trim()) {
148
+ provider = id;
149
+ creds = c;
150
+ break;
151
+ }
152
+ }
153
+ }
154
+ const baseUrl = (creds?.baseUrl || "").trim();
155
+ const apiKey = (creds?.apiKey ?? "").trim();
156
+ if (!baseUrl || !apiKey) {
157
+ res.writeHead(502, { "Content-Type": "application/json" });
158
+ res.end(JSON.stringify({ error: "No upstream" }));
159
+ return;
160
+ }
161
+ const base = baseUrl.replace(/\/$/, "");
162
+ const pathFromOpenClaw = (url.split("?")[0] || "/").trim() || "/";
163
+ const targetUrl = base + (pathFromOpenClaw.startsWith("/") ? pathFromOpenClaw : "/" + pathFromOpenClaw);
164
+ const currentModel = openaiRequest.model || "";
165
+ if (!currentModel || currentModel === "auto" || currentModel.startsWith("memoryx-proxy/")) {
166
+ const firstId = creds?.models?.[0]?.id || creds?.models?.[0]?.name;
167
+ if (firstId)
168
+ openaiRequest.model = firstId;
169
+ }
170
+ let memoryxApiKey = "";
171
+ let agentId = "";
172
+ try {
173
+ const sdk = await getSDK(pluginConfig);
174
+ const accountInfo = await sdk.getAccountInfo();
175
+ memoryxApiKey = accountInfo.apiKey || "";
176
+ agentId = accountInfo.agentId || "";
177
+ }
178
+ catch (_e) {
179
+ /* Do not block; proxy will fail and then fall back to direct */
180
+ }
181
+ const headers = this.buildForwardHeaders(provider, apiKey);
182
+ const messages = openaiRequest.messages || [];
183
+ const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
184
+ const searchQuery = typeof lastUserMsg?.content === "string" ? lastUserMsg.content : "";
185
+ const proxyRequestBody = {
186
+ targetUrl,
187
+ headers,
188
+ body: openaiRequest,
189
+ searchQuery,
190
+ agent_id: agentId,
191
+ available_models: this.availableProviders.map((p) => `${p.provider}/${p.model}`),
192
+ };
193
+ const proxyReqHeaders = { "Content-Type": "application/json", "X-API-Key": memoryxApiKey ? "***" : "" };
194
+ log(`[Sidecar] ${reqId} → Proxy POST ${proxyUrl}`);
195
+ log(`[Sidecar] ${reqId} Proxy request headers: ${JSON.stringify(proxyReqHeaders)}`);
196
+ log(`[Sidecar] ${reqId} Proxy body: targetUrl=${targetUrl} model=${openaiRequest.model} stream=${!!openaiRequest.stream} searchQuery.length=${searchQuery.length} agent_id=${agentId || "(empty)"}`);
197
+ log(`[Sidecar] ${reqId} Forward headers (to upstream): ${JSON.stringify(this.redactHeaders(headers))}`);
198
+ let proxyResponse = null;
199
+ try {
200
+ proxyResponse = await fetch(proxyUrl, {
201
+ method: "POST",
202
+ headers: { "Content-Type": "application/json", "X-API-Key": memoryxApiKey },
203
+ body: JSON.stringify(proxyRequestBody),
204
+ });
205
+ }
206
+ catch (e) {
207
+ proxyResponse = null;
208
+ log(`[Sidecar] ${reqId} Proxy fetch error: ${e?.message ?? String(e)}`, { console: true });
209
+ }
210
+ if (proxyResponse) {
211
+ log(`[Sidecar] ${reqId} Proxy response status=${proxyResponse.status} ${proxyResponse.statusText}`);
212
+ }
213
+ const proxyFailed = !proxyResponse || proxyResponse.status >= 500;
214
+ if (proxyFailed) {
215
+ log(`[Sidecar] ${reqId} Proxy failed, fallback to direct POST ${targetUrl}`, { console: true });
216
+ const directResponse = await this.directRequest(reqId, targetUrl, headers, openaiRequest);
217
+ this.writeResponse(res, directResponse);
218
+ return;
219
+ }
220
+ const proxy = proxyResponse;
221
+ if (openaiRequest.stream && proxy.body) {
222
+ const reader = proxy.body.getReader();
223
+ let firstChunk;
224
+ try {
225
+ firstChunk = await Promise.race([
226
+ reader.read(),
227
+ new Promise((_, rej) => setTimeout(() => rej(new Error("First chunk timeout")), 15000)),
228
+ ]);
229
+ }
230
+ catch (firstErr) {
231
+ reader.releaseLock();
232
+ log(`[Sidecar] ${reqId} Proxy stream first chunk failed: ${firstErr?.message ?? "timeout"}, fallback to direct ${targetUrl}`, { console: true });
233
+ const directResponse = await this.directRequest(reqId, targetUrl, headers, openaiRequest);
234
+ if (directResponse)
235
+ log(`[Sidecar] ${reqId} Direct response status=${directResponse.status}`);
236
+ this.writeResponse(res, directResponse);
237
+ return;
238
+ }
239
+ res.writeHead(proxy.status, {
240
+ "Content-Type": proxy.headers.get("content-type") || "application/json",
241
+ });
242
+ try {
243
+ if (!firstChunk.done && firstChunk.value)
244
+ res.write(Buffer.from(firstChunk.value));
245
+ while (true) {
246
+ const { done, value } = await reader.read();
247
+ if (done)
248
+ break;
249
+ res.write(Buffer.from(value));
250
+ }
251
+ }
252
+ finally {
253
+ reader.releaseLock();
254
+ }
255
+ res.end();
256
+ log(`[Sidecar] ${reqId} Proxy stream done, status=${proxy.status}`);
257
+ return;
258
+ }
259
+ if (!proxy.body) {
260
+ res.writeHead(proxy.status, {
261
+ "Content-Type": proxy.headers.get("content-type") || "application/json",
262
+ });
263
+ res.end();
264
+ return;
265
+ }
266
+ const text = await proxy.text();
267
+ log(`[Sidecar] ${reqId} Proxy done, status=${proxy.status} body.length=${text.length}`);
268
+ res.writeHead(proxy.status, {
269
+ "Content-Type": proxy.headers.get("content-type") || "application/json",
270
+ });
271
+ res.end(text);
272
+ }
273
+ async directRequest(reqId, targetUrl, headers, openaiRequest) {
274
+ log(`[Sidecar] ${reqId} Direct POST ${targetUrl} headers=${JSON.stringify(this.redactHeaders(headers))} model=${openaiRequest?.model} stream=${!!openaiRequest?.stream}`);
275
+ try {
276
+ const resp = await fetch(targetUrl, {
277
+ method: "POST",
278
+ headers,
279
+ body: JSON.stringify(openaiRequest),
280
+ });
281
+ log(`[Sidecar] ${reqId} Direct response status=${resp.status} ${resp.statusText}`);
282
+ return resp;
283
+ }
284
+ catch (e) {
285
+ log(`[Sidecar] ${reqId} Direct fetch error: ${e?.message ?? String(e)}`, { console: true });
286
+ return new Response(JSON.stringify({ error: e?.message || "Direct request failed" }), {
287
+ status: 502,
288
+ headers: { "Content-Type": "application/json" },
289
+ });
290
+ }
291
+ }
292
+ async writeResponse(res, directResponse) {
293
+ const contentType = directResponse.headers.get("content-type") || "application/json";
294
+ res.writeHead(directResponse.status, { "Content-Type": contentType });
295
+ const body = directResponse.body;
296
+ if (!body) {
297
+ res.end();
298
+ return;
299
+ }
300
+ const reader = body.getReader();
301
+ try {
302
+ while (true) {
303
+ const { done, value } = await reader.read();
304
+ if (done)
305
+ break;
306
+ res.write(Buffer.from(value));
307
+ }
308
+ }
309
+ finally {
310
+ reader.releaseLock();
311
+ }
312
+ res.end();
313
+ }
314
+ readBody(req) {
315
+ return new Promise((resolve, reject) => {
316
+ const chunks = [];
317
+ req.on("data", (chunk) => chunks.push(chunk));
318
+ req.on("end", () => resolve(Buffer.concat(chunks).toString()));
319
+ req.on("error", reject);
320
+ });
321
+ }
322
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * MemoryX OpenClaw tools: recall, forget, store, list, account_info, queue_status.
3
+ */
4
+ import type { MemoryXPlugin } from "./plugin-core.js";
5
+ export declare function registerTools(api: any, plugin: MemoryXPlugin): void;
6
+ //# sourceMappingURL=tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,wBAAgB,aAAa,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,aAAa,GAAG,IAAI,CAkXnE"}