@digitalforgestudios/openclaw-sulcus 1.5.3 → 2.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/index.ts +90 -248
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -31,7 +31,7 @@ Memories survive across sessions. They have heat (0.0–1.0) that decays over ti
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
// Legacy static string for backward compat (overwritten at register time)
|
|
34
|
-
let STATIC_AWARENESS = buildStaticAwareness("
|
|
34
|
+
let STATIC_AWARENESS = buildStaticAwareness("local", "default", "local");
|
|
35
35
|
|
|
36
36
|
// Fallback context when build_context fails — includes the cheatsheet
|
|
37
37
|
// but warns that dynamic context is unavailable.
|
|
@@ -49,33 +49,46 @@ const FALLBACK_AWARENESS = `<sulcus_context token_budget="500">
|
|
|
49
49
|
</cheatsheet>
|
|
50
50
|
</sulcus_context>`;
|
|
51
51
|
|
|
52
|
-
//
|
|
52
|
+
// MCP-over-stdio client for sulcus-local
|
|
53
|
+
// This is the ONLY communication path — no REST/network calls in this plugin.
|
|
54
|
+
// If serverUrl/apiKey are configured, they are passed as env vars to the spawn()
|
|
55
|
+
// call below so that sulcus-sync (a dylib loaded by sulcus-local) can pick them
|
|
56
|
+
// up for cloud replication. The plugin itself never makes any HTTP calls.
|
|
53
57
|
class SulcusClient {
|
|
54
58
|
private child: ChildProcess | null = null;
|
|
55
59
|
private nextId = 1;
|
|
56
60
|
private pending = new Map<string | number, (res: any) => void>();
|
|
57
61
|
private configPath: string | undefined;
|
|
62
|
+
private spawnEnv: Record<string, string>;
|
|
58
63
|
|
|
59
|
-
constructor(
|
|
64
|
+
constructor(
|
|
65
|
+
private binaryPath: string,
|
|
66
|
+
configPath?: string,
|
|
67
|
+
spawnEnv: Record<string, string> = {}
|
|
68
|
+
) {
|
|
60
69
|
this.configPath = configPath;
|
|
70
|
+
this.spawnEnv = spawnEnv;
|
|
61
71
|
}
|
|
62
72
|
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
// (local mode). When serverUrl is set, REST API is used instead (no spawn).
|
|
73
|
+
// Launches sulcus-local binary as an MCP stdio sidecar.
|
|
74
|
+
// serverUrl/apiKey (if any) are passed via environment variables so that
|
|
75
|
+
// sulcus-sync (dylib) can perform cloud replication without this plugin
|
|
76
|
+
// making any network calls itself.
|
|
68
77
|
async start(configPath?: string) {
|
|
69
78
|
const cfgPath = configPath || this.configPath;
|
|
70
79
|
const args = cfgPath ? ["--config", cfgPath, "stdio"] : ["stdio"];
|
|
71
80
|
this.child = spawn(this.binaryPath, args, {
|
|
72
81
|
stdio: ["pipe", "pipe", "inherit"],
|
|
73
|
-
env: {
|
|
82
|
+
env: {
|
|
83
|
+
...process.env,
|
|
84
|
+
RUST_LOG: "info",
|
|
85
|
+
...this.spawnEnv, // SULCUS_SERVER_URL and SULCUS_API_KEY forwarded here
|
|
86
|
+
}
|
|
74
87
|
});
|
|
75
88
|
|
|
76
89
|
this.child.on("error", (err) => {
|
|
77
90
|
// Reject all pending calls if the process dies
|
|
78
|
-
for (const [
|
|
91
|
+
for (const [_id, resolve] of this.pending) {
|
|
79
92
|
resolve({ error: { code: -1, message: `Sulcus process error: ${err.message}` } });
|
|
80
93
|
}
|
|
81
94
|
this.pending.clear();
|
|
@@ -83,7 +96,7 @@ class SulcusClient {
|
|
|
83
96
|
});
|
|
84
97
|
|
|
85
98
|
this.child.on("exit", (code) => {
|
|
86
|
-
for (const [
|
|
99
|
+
for (const [_id, resolve] of this.pending) {
|
|
87
100
|
resolve({ error: { code: -1, message: `Sulcus process exited with code ${code}` } });
|
|
88
101
|
}
|
|
89
102
|
this.pending.clear();
|
|
@@ -114,13 +127,13 @@ class SulcusClient {
|
|
|
114
127
|
clearTimeout(timeout);
|
|
115
128
|
if (res.error) reject(new Error(res.error.message));
|
|
116
129
|
else {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
130
|
+
// MCP result format
|
|
131
|
+
try {
|
|
132
|
+
const content = JSON.parse(res.result.content[0].text);
|
|
133
|
+
resolve(content);
|
|
134
|
+
} catch (e) {
|
|
135
|
+
resolve(res.result);
|
|
136
|
+
}
|
|
124
137
|
}
|
|
125
138
|
});
|
|
126
139
|
this.child!.stdin!.write(JSON.stringify(request) + "\n");
|
|
@@ -132,173 +145,16 @@ class SulcusClient {
|
|
|
132
145
|
}
|
|
133
146
|
}
|
|
134
147
|
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
private logger: any
|
|
142
|
-
) {}
|
|
143
|
-
|
|
144
|
-
private async fetch(path: string, options: RequestInit = {}): Promise<any> {
|
|
145
|
-
const url = `${this.serverUrl}${path}`;
|
|
146
|
-
const headers: Record<string, string> = {
|
|
147
|
-
"Authorization": `Bearer ${this.apiKey}`,
|
|
148
|
-
"Content-Type": "application/json",
|
|
149
|
-
...(options.headers as Record<string, string> || {}),
|
|
150
|
-
};
|
|
151
|
-
const res = await globalThis.fetch(url, { ...options, headers });
|
|
152
|
-
if (!res.ok) {
|
|
153
|
-
const text = await res.text().catch(() => "");
|
|
154
|
-
throw new Error(`Sulcus API ${res.status}: ${text}`);
|
|
155
|
-
}
|
|
156
|
-
return res.json();
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
async searchMemory(query: string, limit: number = 5): Promise<any> {
|
|
160
|
-
const raw = await this.fetch("/api/v1/agent/search", {
|
|
161
|
-
method: "POST",
|
|
162
|
-
body: JSON.stringify({ query, namespace: this.namespace, limit }),
|
|
163
|
-
});
|
|
164
|
-
// Normalize: server returns {results, provenance} — ensure .results is always accessible
|
|
165
|
-
// Also handle flat array (backward compat) and .items/.nodes variants
|
|
166
|
-
const results = raw?.results ?? raw?.items ?? raw?.nodes ?? (Array.isArray(raw) ? raw : []);
|
|
167
|
-
return { results, provenance: raw?.provenance || { backend: "cloud", namespace: this.namespace } };
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
async recordMemory(params: any): Promise<any> {
|
|
171
|
-
const raw = await this.fetch("/api/v1/agent/nodes", {
|
|
172
|
-
method: "POST",
|
|
173
|
-
body: JSON.stringify({
|
|
174
|
-
label: params.content,
|
|
175
|
-
memory_type: params.memory_type || "episodic",
|
|
176
|
-
namespace: params.fold_name || this.namespace,
|
|
177
|
-
heat: 0.8,
|
|
178
|
-
}),
|
|
179
|
-
});
|
|
180
|
-
// Normalize: ensure node_id is accessible (server returns "id")
|
|
181
|
-
return { ...raw, node_id: raw?.id || raw?.node_id };
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
async getMemory(id: string): Promise<any> {
|
|
185
|
-
return this.fetch(`/api/v1/agent/nodes/${id}`);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
async deleteMemory(id: string): Promise<any> {
|
|
189
|
-
return this.fetch(`/api/v1/agent/nodes/${id}`, { method: "DELETE" });
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
async buildContext(prompt: string, tokenBudget: number = 2000): Promise<any> {
|
|
193
|
-
// REST doesn't have a build_context endpoint — search for relevant memories instead
|
|
194
|
-
const res = await this.searchMemory(prompt, 10);
|
|
195
|
-
const results = res?.results || res || [];
|
|
196
|
-
if (!results.length) return null;
|
|
197
|
-
|
|
198
|
-
// Build a simple context string from results
|
|
199
|
-
const items = results.slice(0, 5).map((r: any) =>
|
|
200
|
-
`[${r.memory_type || "memory"}] ${r.pointer_summary || r.label || ""}`
|
|
201
|
-
).join("\n");
|
|
202
|
-
return `<sulcus_context source="cloud" namespace="${this.namespace}">\n${items}\n</sulcus_context>`;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
async getStatus(): Promise<any> {
|
|
206
|
-
return this.fetch("/api/v1/agent/memory/status");
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// ─── CLIENT-SIDE SIU (Semantic Inference Unit) ───────────────────────────────
|
|
211
|
-
// JSON weights classifier: scale → dot → sigmoid. Zero native deps.
|
|
212
|
-
// Downloads model from server on first use, caches locally.
|
|
213
|
-
|
|
214
|
-
interface SiuModel {
|
|
215
|
-
classes: string[];
|
|
216
|
-
scaler_mean: number[];
|
|
217
|
-
scaler_scale: number[];
|
|
218
|
-
coefficients: number[][];
|
|
219
|
-
intercepts: number[];
|
|
220
|
-
n_features: number;
|
|
221
|
-
default_threshold: number;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
class ClientSiu {
|
|
225
|
-
private model: SiuModel | null = null;
|
|
226
|
-
private modelPath: string;
|
|
227
|
-
private serverUrl: string;
|
|
228
|
-
private apiKey: string;
|
|
229
|
-
|
|
230
|
-
constructor(cacheDir: string, serverUrl: string, apiKey: string) {
|
|
231
|
-
this.modelPath = resolve(cacheDir, "memory_classifier_multilabel.json");
|
|
232
|
-
this.serverUrl = serverUrl;
|
|
233
|
-
this.apiKey = apiKey;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// SECURITY NOTE: SIU (Semantic Intelligence Unit) model is a JSON classifier
|
|
237
|
-
// for memory type detection. Downloaded once from the configured Sulcus server,
|
|
238
|
-
// then cached locally at ~/.sulcus/cache/. File read is local cache check only —
|
|
239
|
-
// no user data is sent. The download sends only the API key for auth, not file
|
|
240
|
-
// contents. This is a standard model-caching pattern (like downloading an ONNX model).
|
|
241
|
-
async ensureModel(): Promise<SiuModel | null> {
|
|
242
|
-
if (this.model) return this.model;
|
|
243
|
-
const { existsSync, readFileSync, writeFileSync, mkdirSync } = require("node:fs");
|
|
244
|
-
const { dirname } = require("node:path");
|
|
245
|
-
|
|
246
|
-
// Try loading cached model — local file read, no network
|
|
247
|
-
if (existsSync(this.modelPath)) {
|
|
248
|
-
try {
|
|
249
|
-
this.model = JSON.parse(readFileSync(this.modelPath, "utf8"));
|
|
250
|
-
return this.model;
|
|
251
|
-
} catch { }
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Download from server
|
|
255
|
-
if (!this.serverUrl || !this.apiKey) return null;
|
|
256
|
-
try {
|
|
257
|
-
const res = await globalThis.fetch(
|
|
258
|
-
`${this.serverUrl}/api/v1/agent/siu-model`,
|
|
259
|
-
{ headers: { "Authorization": `Bearer ${this.apiKey}` } }
|
|
260
|
-
);
|
|
261
|
-
if (res.ok) {
|
|
262
|
-
const data = await res.json();
|
|
263
|
-
mkdirSync(dirname(this.modelPath), { recursive: true });
|
|
264
|
-
writeFileSync(this.modelPath, JSON.stringify(data));
|
|
265
|
-
this.model = data;
|
|
266
|
-
return this.model;
|
|
267
|
-
}
|
|
268
|
-
} catch { }
|
|
269
|
-
return null;
|
|
270
|
-
}
|
|
148
|
+
// NOTE: SulcusRestClient was removed — the plugin no longer makes any HTTP/REST
|
|
149
|
+
// calls. All communication with sulcus-local goes through MCP over stdio via
|
|
150
|
+
// SulcusClient above. If serverUrl/apiKey are provided in config, they are
|
|
151
|
+
// forwarded as SULCUS_SERVER_URL / SULCUS_API_KEY environment variables to the
|
|
152
|
+
// sulcus-local spawn so that sulcus-sync (dylib) can replicate to the cloud
|
|
153
|
+
// without this plugin touching the network.
|
|
271
154
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const m = this.model;
|
|
276
|
-
if (embedding.length !== m.n_features) return null;
|
|
277
|
-
|
|
278
|
-
// Scale: (x - mean) / scale
|
|
279
|
-
const scaled = embedding.map((x, i) => (x - m.scaler_mean[i]) / m.scaler_scale[i]);
|
|
280
|
-
|
|
281
|
-
// Dot product + sigmoid for each class
|
|
282
|
-
const scores: Record<string, number> = {};
|
|
283
|
-
let bestType = "episodic";
|
|
284
|
-
let bestScore = 0;
|
|
285
|
-
|
|
286
|
-
for (let c = 0; c < m.classes.length; c++) {
|
|
287
|
-
let dot = m.intercepts[c];
|
|
288
|
-
for (let f = 0; f < m.n_features; f++) {
|
|
289
|
-
dot += scaled[f] * m.coefficients[c][f];
|
|
290
|
-
}
|
|
291
|
-
const sigmoid = 1 / (1 + Math.exp(-dot));
|
|
292
|
-
scores[m.classes[c]] = sigmoid;
|
|
293
|
-
if (sigmoid > bestScore) {
|
|
294
|
-
bestScore = sigmoid;
|
|
295
|
-
bestType = m.classes[c];
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
return { type: bestType, confidence: bestScore, all: scores };
|
|
300
|
-
}
|
|
301
|
-
}
|
|
155
|
+
// NOTE: SiuClassifier / ClientSiu / SiuModel were removed — sulcus-local
|
|
156
|
+
// already has fastembed + ONNX for embeddings and handles classification
|
|
157
|
+
// internally. A future PR may expose classification via an MCP tool if needed.
|
|
302
158
|
|
|
303
159
|
// ─── PRE-SEND FILTER ─────────────────────────────────────────────────────────
|
|
304
160
|
// Rule-based junk filter. Catches obvious noise before it hits the API.
|
|
@@ -342,63 +198,42 @@ const sulcusPlugin = {
|
|
|
342
198
|
const namespace = api.config?.namespace === "default" && agentId
|
|
343
199
|
? agentId
|
|
344
200
|
: (api.config?.namespace || agentId || "default");
|
|
201
|
+
|
|
202
|
+
// serverUrl/apiKey are NOT used by this plugin for HTTP calls.
|
|
203
|
+
// They are forwarded as env vars to sulcus-local so sulcus-sync (dylib)
|
|
204
|
+
// can replicate memories to the cloud without any network calls here.
|
|
345
205
|
const serverUrl = api.config?.serverUrl || "";
|
|
346
206
|
const apiKey = api.config?.apiKey || "";
|
|
347
|
-
const client = new SulcusClient(binaryPath, iniPath);
|
|
348
207
|
|
|
349
|
-
//
|
|
350
|
-
|
|
208
|
+
// Build spawn env: pass cloud config to sulcus-local via environment
|
|
209
|
+
const spawnEnv: Record<string, string> = {};
|
|
210
|
+
if (serverUrl) spawnEnv["SULCUS_SERVER_URL"] = serverUrl;
|
|
211
|
+
if (apiKey) spawnEnv["SULCUS_API_KEY"] = apiKey;
|
|
212
|
+
|
|
213
|
+
const client = new SulcusClient(binaryPath, iniPath, spawnEnv);
|
|
214
|
+
|
|
215
|
+
// Check binary availability
|
|
351
216
|
let hasBinary = false;
|
|
352
217
|
try {
|
|
353
218
|
const { existsSync } = require("node:fs");
|
|
354
219
|
hasBinary = existsSync(binaryPath);
|
|
355
|
-
if (!hasBinary) {
|
|
356
|
-
backendMode = serverUrl ? "cloud" : "unavailable";
|
|
357
|
-
} else {
|
|
358
|
-
backendMode = serverUrl ? "hybrid" : "local";
|
|
359
|
-
}
|
|
360
220
|
} catch { }
|
|
361
221
|
|
|
362
|
-
|
|
363
|
-
const restClient = serverUrl && apiKey
|
|
364
|
-
? new SulcusRestClient(serverUrl, apiKey, namespace, api.logger)
|
|
365
|
-
: null;
|
|
222
|
+
const backendMode = hasBinary ? "local" : "unavailable";
|
|
366
223
|
|
|
367
|
-
//
|
|
224
|
+
// All memory calls go exclusively through MCP — no REST fallback.
|
|
225
|
+
// If the MCP client isn't connected (binary missing or crashed), we return
|
|
226
|
+
// an error rather than falling back to HTTP.
|
|
368
227
|
async function memoryCall(method: string, params: any): Promise<any> {
|
|
369
|
-
if (hasBinary) {
|
|
370
|
-
|
|
371
|
-
return await client.call(method, params);
|
|
372
|
-
} catch (e: any) {
|
|
373
|
-
api.logger.warn(`memory-sulcus: local binary failed (${method}): ${e.message}, falling back to REST`);
|
|
374
|
-
if (!restClient) throw e;
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
if (!restClient) throw new Error("Sulcus unavailable: no binary and no server configured");
|
|
378
|
-
// Route to appropriate REST method
|
|
379
|
-
switch (method) {
|
|
380
|
-
case "search_memory": return restClient.searchMemory(params.query, params.limit);
|
|
381
|
-
case "record_memory": return restClient.recordMemory(params);
|
|
382
|
-
case "build_context": return restClient.buildContext(params.prompt, params.token_budget);
|
|
383
|
-
default: throw new Error(`Unknown method: ${method}`);
|
|
228
|
+
if (!hasBinary) {
|
|
229
|
+
throw new Error(`Sulcus unavailable: binary not found at ${binaryPath}`);
|
|
384
230
|
}
|
|
231
|
+
return client.call(method, params);
|
|
385
232
|
}
|
|
386
233
|
|
|
387
234
|
// Update static awareness with runtime info
|
|
388
235
|
STATIC_AWARENESS = buildStaticAwareness(backendMode, namespace, serverUrl || "local");
|
|
389
236
|
|
|
390
|
-
// Initialize client-side SIU
|
|
391
|
-
const siuCacheDir = resolve(process.env.HOME || "~", ".cache/sulcus/model");
|
|
392
|
-
const clientSiu = serverUrl && apiKey ? new ClientSiu(siuCacheDir, serverUrl, apiKey) : null;
|
|
393
|
-
|
|
394
|
-
// Pre-load SIU model (non-blocking)
|
|
395
|
-
if (clientSiu) {
|
|
396
|
-
clientSiu.ensureModel().then(m => {
|
|
397
|
-
if (m) api.logger.info(`memory-sulcus: client SIU loaded (${m.classes.length} classes, ${m.n_features} features)`);
|
|
398
|
-
else api.logger.warn("memory-sulcus: client SIU model not available");
|
|
399
|
-
}).catch(() => {});
|
|
400
|
-
}
|
|
401
|
-
|
|
402
237
|
api.logger.info(`memory-sulcus: registered (binary: ${binaryPath}, hasBinary: ${hasBinary}, namespace: ${namespace}, backend: ${backendMode})`);
|
|
403
238
|
|
|
404
239
|
// ── Core memory tools ──
|
|
@@ -467,11 +302,11 @@ const sulcusPlugin = {
|
|
|
467
302
|
const provenance = res?.provenance || {
|
|
468
303
|
backend: backendMode,
|
|
469
304
|
namespace,
|
|
470
|
-
server: serverUrl,
|
|
471
|
-
sync_available:
|
|
305
|
+
server: serverUrl || "local",
|
|
306
|
+
sync_available: !!serverUrl,
|
|
472
307
|
siu_classified: false,
|
|
473
308
|
};
|
|
474
|
-
const provenanceStr = `[${provenance.backend}] namespace: ${provenance.namespace || namespace}, server: ${provenance.server ||
|
|
309
|
+
const provenanceStr = `[${provenance.backend}] namespace: ${provenance.namespace || namespace}, server: ${provenance.server || "local"}`;
|
|
475
310
|
return {
|
|
476
311
|
content: [{ type: "text", text: `Stored [${params.memory_type || "episodic"}] memory: "${(params.content || "").substring(0, 80)}..." → ${provenanceStr}` }],
|
|
477
312
|
details: { ...res, provenance }
|
|
@@ -485,28 +320,35 @@ const sulcusPlugin = {
|
|
|
485
320
|
description: "Check Sulcus memory backend status: connection, namespace, capabilities, and memory count.",
|
|
486
321
|
parameters: Type.Object({}),
|
|
487
322
|
async execute(_id: string, _params: any) {
|
|
488
|
-
if (
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
323
|
+
if (!hasBinary) {
|
|
324
|
+
return {
|
|
325
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
326
|
+
status: "unavailable",
|
|
327
|
+
backend: backendMode,
|
|
328
|
+
namespace,
|
|
329
|
+
binary: binaryPath,
|
|
330
|
+
server: serverUrl || "none (env forwarding only)",
|
|
331
|
+
}, null, 2) }],
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
try {
|
|
335
|
+
// Ask sulcus-local for status via MCP
|
|
336
|
+
const status = await memoryCall("memory_status", {});
|
|
337
|
+
return {
|
|
338
|
+
content: [{ type: "text", text: JSON.stringify(status, null, 2) }],
|
|
339
|
+
details: status
|
|
340
|
+
};
|
|
341
|
+
} catch (e: any) {
|
|
342
|
+
return {
|
|
343
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
344
|
+
status: "error",
|
|
345
|
+
backend: backendMode,
|
|
346
|
+
namespace,
|
|
347
|
+
server: serverUrl || "none (env forwarding only)",
|
|
348
|
+
error: e.message,
|
|
349
|
+
}, null, 2) }],
|
|
350
|
+
};
|
|
500
351
|
}
|
|
501
|
-
// Fallback: return local config info
|
|
502
|
-
return {
|
|
503
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
504
|
-
status: hasBinary ? "local" : "unavailable",
|
|
505
|
-
backend: backendMode,
|
|
506
|
-
namespace,
|
|
507
|
-
server: serverUrl || "none",
|
|
508
|
-
}, null, 2) }],
|
|
509
|
-
};
|
|
510
352
|
}
|
|
511
353
|
}, { name: "memory_status" });
|
|
512
354
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@digitalforgestudios/openclaw-sulcus",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.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",
|