@digitalforgestudios/openclaw-sulcus 3.4.0 → 3.5.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/index.ts +172 -0
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
+
import * as https from "node:https";
|
|
4
|
+
import * as http from "node:http";
|
|
5
|
+
import { URL } from "node:url";
|
|
3
6
|
import { Type } from "@sinclair/typebox";
|
|
4
7
|
|
|
5
8
|
// ─── STATIC AWARENESS ───────────────────────────────────────────────────────
|
|
@@ -128,6 +131,157 @@ const hookHandlers: Record<string, HookHandler> = {
|
|
|
128
131
|
},
|
|
129
132
|
};
|
|
130
133
|
|
|
134
|
+
// ─── CLOUD HTTP CLIENT ───────────────────────────────────────────────────────
|
|
135
|
+
// Lightweight fallback client for users without local dylibs/WASM.
|
|
136
|
+
// Uses Node.js built-in https/http — ZERO external dependencies.
|
|
137
|
+
// Activates only when serverUrl + apiKey are configured and local libs are absent.
|
|
138
|
+
|
|
139
|
+
class SulcusCloudClient {
|
|
140
|
+
private serverUrl: string;
|
|
141
|
+
private apiKey: string;
|
|
142
|
+
|
|
143
|
+
constructor(serverUrl: string, apiKey: string) {
|
|
144
|
+
// Strip trailing slash for clean path concatenation
|
|
145
|
+
this.serverUrl = serverUrl.replace(/\/+$/, "");
|
|
146
|
+
this.apiKey = apiKey;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Low-level HTTP helper. Returns parsed JSON response body. */
|
|
150
|
+
private request(method: string, path: string, body?: any): Promise<any> {
|
|
151
|
+
return new Promise((resolve, reject) => {
|
|
152
|
+
let parsedUrl: URL;
|
|
153
|
+
try {
|
|
154
|
+
parsedUrl = new URL(this.serverUrl + path);
|
|
155
|
+
} catch (e: any) {
|
|
156
|
+
return reject(new Error(`SulcusCloudClient: invalid URL ${this.serverUrl}${path}: ${e.message}`));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const isHttps = parsedUrl.protocol === "https:";
|
|
160
|
+
const transport = isHttps ? https : http;
|
|
161
|
+
|
|
162
|
+
const bodyStr = body !== undefined ? JSON.stringify(body) : undefined;
|
|
163
|
+
const headers: Record<string, string> = {
|
|
164
|
+
"Authorization": `Bearer ${this.apiKey}`,
|
|
165
|
+
"Accept": "application/json",
|
|
166
|
+
};
|
|
167
|
+
if (bodyStr !== undefined) {
|
|
168
|
+
headers["Content-Type"] = "application/json";
|
|
169
|
+
headers["Content-Length"] = String(Buffer.byteLength(bodyStr));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const options = {
|
|
173
|
+
hostname: parsedUrl.hostname,
|
|
174
|
+
port: parsedUrl.port ? parseInt(parsedUrl.port, 10) : (isHttps ? 443 : 80),
|
|
175
|
+
path: parsedUrl.pathname + parsedUrl.search,
|
|
176
|
+
method,
|
|
177
|
+
headers,
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const req = transport.request(options, (res) => {
|
|
181
|
+
const chunks: Buffer[] = [];
|
|
182
|
+
res.on("data", (chunk: Buffer) => chunks.push(chunk));
|
|
183
|
+
res.on("end", () => {
|
|
184
|
+
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
185
|
+
if (!res.statusCode || res.statusCode >= 400) {
|
|
186
|
+
return reject(new Error(`SulcusCloudClient: HTTP ${res.statusCode} for ${method} ${path}: ${raw.substring(0, 200)}`));
|
|
187
|
+
}
|
|
188
|
+
if (!raw || raw.trim() === "") {
|
|
189
|
+
return resolve(null);
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
resolve(JSON.parse(raw));
|
|
193
|
+
} catch (_e) {
|
|
194
|
+
// Some endpoints return plain text (e.g. markdown export)
|
|
195
|
+
resolve(raw);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
req.on("error", (e: Error) => reject(new Error(`SulcusCloudClient: network error for ${method} ${path}: ${e.message}`)));
|
|
201
|
+
|
|
202
|
+
if (bodyStr !== undefined) {
|
|
203
|
+
req.write(bodyStr);
|
|
204
|
+
}
|
|
205
|
+
req.end();
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* search_memory — maps to POST /agent/search
|
|
211
|
+
* Server returns { results: [...] }; we normalise to the results array.
|
|
212
|
+
*/
|
|
213
|
+
async search_memory(query: string, limit?: number): Promise<{ results: any[] }> {
|
|
214
|
+
const body: any = { query };
|
|
215
|
+
if (limit !== undefined) body.limit = limit;
|
|
216
|
+
const res = await this.request("POST", "/agent/search", body);
|
|
217
|
+
const results = res?.results ?? res?.items ?? res?.nodes ?? (Array.isArray(res) ? res : []);
|
|
218
|
+
return { results };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* add_memory — maps to POST /agent/nodes
|
|
223
|
+
* Server returns { id, ... }; pass through.
|
|
224
|
+
*/
|
|
225
|
+
async add_memory(content: string, memoryType?: string | null): Promise<{ id: string; [key: string]: any }> {
|
|
226
|
+
const body: any = { text: content };
|
|
227
|
+
if (memoryType) body.memory_type = memoryType;
|
|
228
|
+
const res = await this.request("POST", "/agent/nodes", body);
|
|
229
|
+
return res ?? { id: "unknown" };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* list_hot_nodes — maps to GET /agent/memory/status
|
|
234
|
+
* Returns hot_nodes list; normalised for memory_status tool.
|
|
235
|
+
*/
|
|
236
|
+
async list_hot_nodes(_limit?: number): Promise<{ nodes: any[] }> {
|
|
237
|
+
const res = await this.request("GET", "/agent/memory/status");
|
|
238
|
+
const nodes = res?.hot_nodes ?? res?.nodes ?? [];
|
|
239
|
+
return { nodes };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* consolidate — maps to POST /agent/consolidate
|
|
244
|
+
*/
|
|
245
|
+
async consolidate(minHeat?: number): Promise<any> {
|
|
246
|
+
const body: any = {};
|
|
247
|
+
if (minHeat !== undefined) body.min_heat = minHeat;
|
|
248
|
+
return this.request("POST", "/agent/consolidate", body);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* export_markdown — maps to GET /agent/export?format=markdown
|
|
253
|
+
* Returns raw markdown string.
|
|
254
|
+
*/
|
|
255
|
+
async export_markdown(): Promise<string> {
|
|
256
|
+
const res = await this.request("GET", "/agent/export?format=markdown");
|
|
257
|
+
// Server may return { content: "..." } or raw string
|
|
258
|
+
if (typeof res === "string") return res;
|
|
259
|
+
return res?.content ?? res?.markdown ?? JSON.stringify(res, null, 2);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* import_markdown — maps to POST /agent/import
|
|
264
|
+
*/
|
|
265
|
+
async import_markdown(text: string): Promise<any> {
|
|
266
|
+
return this.request("POST", "/agent/import", { format: "markdown", content: text });
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* evaluate_triggers — maps to POST /agent/triggers/evaluate
|
|
271
|
+
*/
|
|
272
|
+
async evaluate_triggers(event: any, contextJson?: string): Promise<any> {
|
|
273
|
+
const body: any = { event };
|
|
274
|
+
if (contextJson) {
|
|
275
|
+
try {
|
|
276
|
+
body.context = JSON.parse(contextJson);
|
|
277
|
+
} catch (_e) {
|
|
278
|
+
body.context = contextJson;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return this.request("POST", "/agent/triggers/evaluate", body);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
131
285
|
// ─── NATIVE LIB LOADER ──────────────────────────────────────────────────────
|
|
132
286
|
// Loads libsulcus_store.dylib (embedded PG) and libsulcus_vectors.dylib (embeddings)
|
|
133
287
|
// via koffi FFI. Provides queryFn and embedFn callbacks for SulcusMem.create().
|
|
@@ -603,6 +757,10 @@ const sulcusPlugin = {
|
|
|
603
757
|
? resolve(api.config.wasmDir)
|
|
604
758
|
: resolve(__dirname, "wasm");
|
|
605
759
|
|
|
760
|
+
// Cloud fallback credentials (used when local libs unavailable)
|
|
761
|
+
const serverUrl: string | undefined = api.config?.serverUrl;
|
|
762
|
+
const apiKey: string | undefined = api.config?.apiKey;
|
|
763
|
+
|
|
606
764
|
// Default namespace = agent name (prevents everything landing in "default")
|
|
607
765
|
const agentId = api.config?.agentId || api.pluginConfig?.agentId;
|
|
608
766
|
const namespace = api.config?.namespace === "default" && agentId
|
|
@@ -644,6 +802,20 @@ const sulcusPlugin = {
|
|
|
644
802
|
api.logger.warn(`sulcus: native libs unavailable — ${nativeLoader.error}`);
|
|
645
803
|
}
|
|
646
804
|
|
|
805
|
+
// ── Cloud HTTP fallback ──
|
|
806
|
+
// Activates only when local WASM/native libs are unavailable AND
|
|
807
|
+
// serverUrl + apiKey are configured. Zero external dependencies.
|
|
808
|
+
if (sulcusMem === null && serverUrl && apiKey) {
|
|
809
|
+
try {
|
|
810
|
+
const cloudClient = new SulcusCloudClient(serverUrl, apiKey);
|
|
811
|
+
sulcusMem = cloudClient;
|
|
812
|
+
backendMode = "cloud";
|
|
813
|
+
api.logger.info(`sulcus: using cloud backend (server: ${serverUrl})`);
|
|
814
|
+
} catch (e: any) {
|
|
815
|
+
api.logger.warn(`sulcus: cloud client init failed: ${e.message}`);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
647
819
|
const isAvailable = sulcusMem !== null;
|
|
648
820
|
|
|
649
821
|
// Update static awareness with runtime info
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@digitalforgestudios/openclaw-sulcus",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.0",
|
|
4
4
|
"description": "Sulcus — reactive, thermodynamic memory plugin for OpenClaw. Opt-in persistent memory with heat-based decay, semantic search, and cross-agent sync. Auto-recall and auto-capture disabled by default.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openclaw",
|