@digitalforgestudios/openclaw-sulcus 3.3.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/bin/configure.mjs +246 -19
- package/index.ts +172 -0
- package/package.json +1 -1
package/bin/configure.mjs
CHANGED
|
@@ -12,6 +12,8 @@ import readline from 'readline';
|
|
|
12
12
|
import fs from 'fs';
|
|
13
13
|
import path from 'path';
|
|
14
14
|
import os from 'os';
|
|
15
|
+
import https from 'https';
|
|
16
|
+
import { execSync } from 'child_process';
|
|
15
17
|
|
|
16
18
|
// ─── Colour support ───────────────────────────────────────────────────────────
|
|
17
19
|
|
|
@@ -153,6 +155,208 @@ function deepMerge(target, source) {
|
|
|
153
155
|
return target;
|
|
154
156
|
}
|
|
155
157
|
|
|
158
|
+
// ─── Prebuilt binary download ─────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Detect the current platform slug used in GitHub release asset names.
|
|
162
|
+
* Returns { platform, ext } or throws if unsupported.
|
|
163
|
+
*/
|
|
164
|
+
function detectPlatform() {
|
|
165
|
+
const plat = process.platform;
|
|
166
|
+
const arch = process.arch;
|
|
167
|
+
|
|
168
|
+
const ext = plat === 'darwin' ? '.dylib' : '.so';
|
|
169
|
+
|
|
170
|
+
if (plat === 'darwin' && arch === 'arm64') return { platform: 'macos-arm64', ext };
|
|
171
|
+
if (plat === 'darwin' && arch === 'x64') return { platform: 'macos-x64', ext };
|
|
172
|
+
if (plat === 'linux' && arch === 'x64') return { platform: 'linux-x64', ext };
|
|
173
|
+
if (plat === 'linux' && arch === 'arm64') return { platform: 'linux-arm64', ext };
|
|
174
|
+
|
|
175
|
+
throw new Error(
|
|
176
|
+
`Prebuilt binaries are not available for your platform (${plat}/${arch}).\n` +
|
|
177
|
+
` Supported: darwin/arm64, darwin/x64, linux/x64, linux/arm64`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Follow redirects and download `url` into `destFile`.
|
|
183
|
+
* Shows a simple percentage progress bar (or dots when content-length is unknown).
|
|
184
|
+
* Follows up to maxRedirects hops.
|
|
185
|
+
*/
|
|
186
|
+
function downloadFile(url, destFile, maxRedirects = 5) {
|
|
187
|
+
return new Promise((resolve, reject) => {
|
|
188
|
+
let hops = 0;
|
|
189
|
+
|
|
190
|
+
function attempt(currentUrl) {
|
|
191
|
+
if (hops > maxRedirects) {
|
|
192
|
+
return reject(new Error('Too many redirects while downloading'));
|
|
193
|
+
}
|
|
194
|
+
hops++;
|
|
195
|
+
|
|
196
|
+
const parsed = new URL(currentUrl);
|
|
197
|
+
const opts = {
|
|
198
|
+
hostname: parsed.hostname,
|
|
199
|
+
path: parsed.pathname + parsed.search,
|
|
200
|
+
method: 'GET',
|
|
201
|
+
headers: { 'User-Agent': 'sulcus-configure/1.0' },
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const req = https.request(opts, (res) => {
|
|
205
|
+
const { statusCode, headers: resHeaders } = res;
|
|
206
|
+
|
|
207
|
+
// Follow 301/302/307/308 redirects
|
|
208
|
+
if (
|
|
209
|
+
(statusCode === 301 || statusCode === 302 ||
|
|
210
|
+
statusCode === 307 || statusCode === 308) &&
|
|
211
|
+
resHeaders.location
|
|
212
|
+
) {
|
|
213
|
+
res.resume(); // drain
|
|
214
|
+
return attempt(resHeaders.location);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (statusCode !== 200) {
|
|
218
|
+
res.resume();
|
|
219
|
+
return reject(new Error(`HTTP ${statusCode} for ${currentUrl}`));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const total = parseInt(resHeaders['content-length'] || '0', 10);
|
|
223
|
+
let received = 0;
|
|
224
|
+
let lastPct = -1;
|
|
225
|
+
|
|
226
|
+
const out = fs.createWriteStream(destFile);
|
|
227
|
+
|
|
228
|
+
res.on('data', (chunk) => {
|
|
229
|
+
received += chunk.length;
|
|
230
|
+
out.write(chunk);
|
|
231
|
+
|
|
232
|
+
if (total > 0) {
|
|
233
|
+
const pct = Math.floor((received / total) * 100);
|
|
234
|
+
if (pct !== lastPct && pct % 5 === 0) {
|
|
235
|
+
lastPct = pct;
|
|
236
|
+
process.stdout.write(`\r Downloading... ${pct}% `);
|
|
237
|
+
}
|
|
238
|
+
} else {
|
|
239
|
+
// No content-length — show dots
|
|
240
|
+
if (received % (64 * 1024) === 0) process.stdout.write('.');
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
res.on('end', () => {
|
|
245
|
+
out.end(() => {
|
|
246
|
+
process.stdout.write(`\r Downloaded ${(received / 1024 / 1024).toFixed(1)} MB \n`);
|
|
247
|
+
resolve();
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
res.on('error', (err) => {
|
|
252
|
+
out.destroy();
|
|
253
|
+
reject(err);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
req.on('error', reject);
|
|
258
|
+
req.end();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
attempt(url);
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Download and install prebuilt dylibs for the current platform.
|
|
267
|
+
* Returns true on success, false if the user skips or something goes wrong.
|
|
268
|
+
*
|
|
269
|
+
* @param {string} resolvedLibDir Absolute path where dylibs should be placed
|
|
270
|
+
* @param {string[]} dylibNames Base names without extension, e.g. ['libsulcus_store', ...]
|
|
271
|
+
*/
|
|
272
|
+
async function downloadAndInstallBinaries(resolvedLibDir, dylibNames) {
|
|
273
|
+
let platformInfo;
|
|
274
|
+
try {
|
|
275
|
+
platformInfo = detectPlatform();
|
|
276
|
+
} catch (err) {
|
|
277
|
+
console.log(` ${yellow('⚠')} ${err.message}`);
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const { platform, ext } = platformInfo;
|
|
282
|
+
const displayDir = resolvedLibDir.replace(os.homedir(), '~');
|
|
283
|
+
const tarUrl = `https://github.com/digitalforgeca/sulcus/releases/latest/download/sulcus-${platform}.tar.gz`;
|
|
284
|
+
|
|
285
|
+
console.log();
|
|
286
|
+
console.log(` ${yellow('⚠')} Native libraries not found at ${cyan(displayDir)}`);
|
|
287
|
+
console.log(` ${dim(`Download prebuilt binaries for ${bold(platform)}?`)}`);
|
|
288
|
+
|
|
289
|
+
const doDownload = await askYN(`Download prebuilt binaries for ${platform}?`, true);
|
|
290
|
+
if (!doDownload) {
|
|
291
|
+
console.log(` ${dim('Skipped. Install dylibs manually to use Sulcus.')}`);
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Create libDir if needed
|
|
296
|
+
try {
|
|
297
|
+
fs.mkdirSync(resolvedLibDir, { recursive: true });
|
|
298
|
+
} catch (err) {
|
|
299
|
+
console.log(` ${red('✗')} Cannot create ${cyan(resolvedLibDir)}: ${err.message}`);
|
|
300
|
+
console.log(` ${dim('Try running with appropriate permissions.')}`);
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sulcus-'));
|
|
305
|
+
const tarPath = path.join(tmpDir, `sulcus-${platform}.tar.gz`);
|
|
306
|
+
|
|
307
|
+
console.log(` ${dim(`→ ${tarUrl}`)}`);
|
|
308
|
+
process.stdout.write(` Downloading...`);
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
await downloadFile(tarUrl, tarPath);
|
|
312
|
+
} catch (err) {
|
|
313
|
+
console.log(` ${red('✗')} Download failed: ${err.message}`);
|
|
314
|
+
console.log(` ${dim('Check your internet connection or download manually:')}`);
|
|
315
|
+
console.log(` ${cyan(tarUrl)}`);
|
|
316
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {}
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Extract
|
|
321
|
+
console.log(` Extracting...`);
|
|
322
|
+
try {
|
|
323
|
+
execSync(`tar xzf ${JSON.stringify(tarPath)} -C ${JSON.stringify(tmpDir)}`, { stdio: 'pipe' });
|
|
324
|
+
} catch (err) {
|
|
325
|
+
console.log(` ${red('✗')} Extraction failed: ${err.message}`);
|
|
326
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {}
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Move each dylib into libDir
|
|
331
|
+
let allInstalled = true;
|
|
332
|
+
for (const lib of dylibNames) {
|
|
333
|
+
const srcFile = path.join(tmpDir, lib + ext);
|
|
334
|
+
const destFile = path.join(resolvedLibDir, lib + ext);
|
|
335
|
+
|
|
336
|
+
if (!fs.existsSync(srcFile)) {
|
|
337
|
+
console.log(` ${yellow('⚠')} ${lib + ext} not found in tarball`);
|
|
338
|
+
allInstalled = false;
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
try {
|
|
343
|
+
fs.copyFileSync(srcFile, destFile);
|
|
344
|
+
console.log(` ${green('✓')} Installed: ${dim(destFile)}`);
|
|
345
|
+
} catch (err) {
|
|
346
|
+
console.log(` ${red('✗')} Failed to install ${lib + ext}: ${err.message}`);
|
|
347
|
+
if (err.code === 'EACCES') {
|
|
348
|
+
console.log(` ${dim('Try running with appropriate permissions (e.g. sudo).')}`);
|
|
349
|
+
}
|
|
350
|
+
allInstalled = false;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Cleanup temp dir
|
|
355
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {}
|
|
356
|
+
|
|
357
|
+
return allInstalled;
|
|
358
|
+
}
|
|
359
|
+
|
|
156
360
|
// ─── Main wizard ──────────────────────────────────────────────────────────────
|
|
157
361
|
|
|
158
362
|
async function run() {
|
|
@@ -347,7 +551,7 @@ Press ${bold('Enter')} to accept defaults. ${bold('Ctrl+C')} to cancel at any ti
|
|
|
347
551
|
console.log();
|
|
348
552
|
}
|
|
349
553
|
|
|
350
|
-
// ── Step 4: Validate dylib path
|
|
554
|
+
// ── Step 4: Validate dylib path (+ auto-download if missing) ─────────────
|
|
351
555
|
|
|
352
556
|
console.log(`${bold('Step 4 · Validate')}`);
|
|
353
557
|
|
|
@@ -357,30 +561,53 @@ Press ${bold('Enter')} to accept defaults. ${bold('Ctrl+C')} to cancel at any ti
|
|
|
357
561
|
: process.platform === 'win32' ? '.dll'
|
|
358
562
|
: '.so';
|
|
359
563
|
|
|
360
|
-
|
|
564
|
+
/**
|
|
565
|
+
* Check which dylibs are present. Returns true when all are found.
|
|
566
|
+
*/
|
|
567
|
+
function checkDylibs() {
|
|
568
|
+
if (!fs.existsSync(resolvedLibDir)) return false;
|
|
569
|
+
return dylibNames.every((lib) => fs.existsSync(path.join(resolvedLibDir, lib + ext)));
|
|
570
|
+
}
|
|
361
571
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
572
|
+
let dylibsOk = checkDylibs();
|
|
573
|
+
|
|
574
|
+
if (dylibsOk) {
|
|
575
|
+
// All present — just print them
|
|
366
576
|
for (const lib of dylibNames) {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
577
|
+
console.log(` ${green('✓')} Found: ${dim(path.join(resolvedLibDir, lib + ext))}`);
|
|
578
|
+
}
|
|
579
|
+
} else {
|
|
580
|
+
// Some or all missing — try auto-download
|
|
581
|
+
const downloaded = await downloadAndInstallBinaries(resolvedLibDir, dylibNames);
|
|
582
|
+
|
|
583
|
+
if (downloaded) {
|
|
584
|
+
// Re-validate after successful download
|
|
585
|
+
dylibsOk = checkDylibs();
|
|
586
|
+
if (!dylibsOk) {
|
|
587
|
+
console.log(` ${yellow('⚠')} Some dylibs still missing after installation.`);
|
|
373
588
|
}
|
|
589
|
+
} else if (!downloaded) {
|
|
590
|
+
// Download skipped or failed — show manual instructions
|
|
591
|
+
if (fs.existsSync(resolvedLibDir)) {
|
|
592
|
+
// Directory exists but files missing — list what we found / didn't find
|
|
593
|
+
for (const lib of dylibNames) {
|
|
594
|
+
const full = path.join(resolvedLibDir, lib + ext);
|
|
595
|
+
if (fs.existsSync(full)) {
|
|
596
|
+
console.log(` ${green('✓')} Found: ${dim(full)}`);
|
|
597
|
+
} else {
|
|
598
|
+
console.log(` ${yellow('⚠')} Missing: ${dim(full)}`);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
console.log();
|
|
603
|
+
console.log(` ${yellow(bold('Native dylibs missing — Sulcus will not load.'))}`);
|
|
604
|
+
console.log(` Download manually from:`);
|
|
605
|
+
console.log(` ${cyan('https://github.com/digitalforgeca/sulcus/releases/latest')}`);
|
|
606
|
+
console.log(` Or visit: ${cyan('https://sulcus.ca/docs/install')}`);
|
|
374
607
|
}
|
|
375
608
|
}
|
|
376
609
|
|
|
377
|
-
if (
|
|
378
|
-
console.log();
|
|
379
|
-
console.log(` ${yellow(bold('Native dylibs missing — Sulcus will not load.'))}`);;
|
|
380
|
-
console.log(` Run the setup script to download and install them:`);
|
|
381
|
-
console.log(` ${cyan('bash ~/.sulcus/setup-local.sh')}`);
|
|
382
|
-
console.log(` Or visit: ${cyan('https://sulcus.ca/docs/install')}`);
|
|
383
|
-
} else {
|
|
610
|
+
if (dylibsOk) {
|
|
384
611
|
console.log(` ${green('✓')} All dylibs present — Sulcus is ready to go.`);
|
|
385
612
|
}
|
|
386
613
|
|
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",
|