@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.
Files changed (2) hide show
  1. package/bin/configure.mjs +246 -19
  2. 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
- let dylibsOk = true;
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
- if (!fs.existsSync(resolvedLibDir)) {
363
- dylibsOk = false;
364
- console.log(` ${yellow('⚠')} Dylib directory not found: ${cyan(resolvedLibDir)}`);
365
- } else {
572
+ let dylibsOk = checkDylibs();
573
+
574
+ if (dylibsOk) {
575
+ // All present — just print them
366
576
  for (const lib of dylibNames) {
367
- const full = path.join(resolvedLibDir, lib + ext);
368
- if (fs.existsSync(full)) {
369
- console.log(` ${green('✓')} Found: ${dim(full)}`);
370
- } else {
371
- dylibsOk = false;
372
- console.log(` ${yellow('⚠')} Missing: ${dim(full)}`);
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 (!dylibsOk) {
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.0",
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",