@digitalforgestudios/openclaw-sulcus 3.3.0 → 3.4.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/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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@digitalforgestudios/openclaw-sulcus",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.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",
|