@digitalforgestudios/openclaw-sulcus 3.2.2 → 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 +626 -0
  2. package/package.json +5 -1
@@ -0,0 +1,626 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Sulcus Configuration Wizard
4
+ * Interactive CLI to configure the openclaw-sulcus plugin in openclaw.json
5
+ *
6
+ * Usage:
7
+ * npx @digitalforgestudios/openclaw-sulcus configure
8
+ * node bin/configure.mjs [--no-color] [--help]
9
+ */
10
+
11
+ import readline from 'readline';
12
+ import fs from 'fs';
13
+ import path from 'path';
14
+ import os from 'os';
15
+ import https from 'https';
16
+ import { execSync } from 'child_process';
17
+
18
+ // ─── Colour support ───────────────────────────────────────────────────────────
19
+
20
+ const noColor =
21
+ process.argv.includes('--no-color') ||
22
+ process.env.NO_COLOR !== undefined ||
23
+ !process.stdout.isTTY;
24
+
25
+ const c = {
26
+ reset: noColor ? '' : '\x1b[0m',
27
+ bold: noColor ? '' : '\x1b[1m',
28
+ dim: noColor ? '' : '\x1b[2m',
29
+ red: noColor ? '' : '\x1b[31m',
30
+ green: noColor ? '' : '\x1b[32m',
31
+ yellow: noColor ? '' : '\x1b[33m',
32
+ blue: noColor ? '' : '\x1b[34m',
33
+ magenta: noColor ? '' : '\x1b[35m',
34
+ cyan: noColor ? '' : '\x1b[36m',
35
+ white: noColor ? '' : '\x1b[37m',
36
+ };
37
+
38
+ const bold = (s) => `${c.bold}${s}${c.reset}`;
39
+ const dim = (s) => `${c.dim}${s}${c.reset}`;
40
+ const green = (s) => `${c.green}${s}${c.reset}`;
41
+ const yellow = (s) => `${c.yellow}${s}${c.reset}`;
42
+ const red = (s) => `${c.red}${s}${c.reset}`;
43
+ const cyan = (s) => `${c.cyan}${s}${c.reset}`;
44
+ const magenta = (s) => `${c.magenta}${s}${c.reset}`;
45
+
46
+ // ─── Help ─────────────────────────────────────────────────────────────────────
47
+
48
+ if (process.argv.includes('--help') || process.argv.includes('-h')) {
49
+ console.log(`
50
+ ${bold('Sulcus Configuration Wizard')}
51
+
52
+ Interactively configure the openclaw-sulcus plugin inside your openclaw.json.
53
+
54
+ ${bold('Usage:')}
55
+ npx @digitalforgestudios/openclaw-sulcus configure [options]
56
+ node bin/configure.mjs [options]
57
+
58
+ ${bold('Options:')}
59
+ --help, -h Show this help message
60
+ --no-color Disable coloured output
61
+
62
+ ${bold('What it does:')}
63
+ 1. Locates your openclaw.json (checks \$OPENCLAW_CONFIG_PATH, ~/.openclaw/, ./)
64
+ 2. Walks you through backend mode, dylib path, namespace, hooks, and tools
65
+ 3. Deep-merges settings under plugins.entries.openclaw-sulcus.config
66
+ 4. Validates that your native dylibs exist and warns if they are missing
67
+ 5. Reminds you to restart the OpenClaw gateway
68
+
69
+ ${bold('Example:')}
70
+ npx @digitalforgestudios/openclaw-sulcus configure
71
+ `);
72
+ process.exit(0);
73
+ }
74
+
75
+ // ─── Readline helpers ─────────────────────────────────────────────────────────
76
+
77
+ const rl = readline.createInterface({
78
+ input: process.stdin,
79
+ output: process.stdout,
80
+ });
81
+
82
+ // Graceful Ctrl+C
83
+ rl.on('SIGINT', () => {
84
+ console.log(`\n\n${yellow('⚡ Wizard cancelled — no changes were written.')}\n`);
85
+ process.exit(0);
86
+ });
87
+
88
+ /**
89
+ * Prompt the user with an optional default value.
90
+ * Returns the trimmed answer, or the default if empty.
91
+ */
92
+ function ask(question, defaultValue = '') {
93
+ return new Promise((resolve) => {
94
+ const hint = defaultValue !== '' ? dim(` [${defaultValue}]`) : '';
95
+ rl.question(`${question}${hint} `, (answer) => {
96
+ const trimmed = answer.trim();
97
+ resolve(trimmed === '' ? defaultValue : trimmed);
98
+ });
99
+ });
100
+ }
101
+
102
+ /**
103
+ * Ask a yes/no question. Returns boolean.
104
+ * @param {string} question
105
+ * @param {boolean} defaultVal
106
+ */
107
+ function askYN(question, defaultVal = false) {
108
+ return new Promise((resolve) => {
109
+ const hint = dim(` [${defaultVal ? 'Y/n' : 'y/N'}]`);
110
+ rl.question(` ${question}${hint} `, (answer) => {
111
+ const a = answer.trim().toLowerCase();
112
+ if (a === '') resolve(defaultVal);
113
+ else resolve(a === 'y' || a === 'yes');
114
+ });
115
+ });
116
+ }
117
+
118
+ // ─── openclaw.json discovery ──────────────────────────────────────────────────
119
+
120
+ function expandHome(p) {
121
+ if (p.startsWith('~')) return path.join(os.homedir(), p.slice(1));
122
+ return p;
123
+ }
124
+
125
+ function findOpenclawJson() {
126
+ const candidates = [
127
+ process.env.OPENCLAW_CONFIG_PATH,
128
+ path.join(os.homedir(), '.openclaw', 'openclaw.json'),
129
+ path.join(process.cwd(), 'openclaw.json'),
130
+ ].filter(Boolean);
131
+
132
+ for (const candidate of candidates) {
133
+ const resolved = expandHome(candidate);
134
+ if (fs.existsSync(resolved)) return resolved;
135
+ }
136
+ return null;
137
+ }
138
+
139
+ // Deep-merge two plain objects (target mutated).
140
+ function deepMerge(target, source) {
141
+ for (const key of Object.keys(source)) {
142
+ if (
143
+ source[key] !== null &&
144
+ typeof source[key] === 'object' &&
145
+ !Array.isArray(source[key]) &&
146
+ typeof target[key] === 'object' &&
147
+ target[key] !== null &&
148
+ !Array.isArray(target[key])
149
+ ) {
150
+ deepMerge(target[key], source[key]);
151
+ } else {
152
+ target[key] = source[key];
153
+ }
154
+ }
155
+ return target;
156
+ }
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
+
360
+ // ─── Main wizard ──────────────────────────────────────────────────────────────
361
+
362
+ async function run() {
363
+ console.log(`
364
+ ${bold(magenta('🧠 Sulcus Configuration Wizard'))}
365
+ ${dim('────────────────────────────────────────────')}
366
+ Configures the ${cyan('openclaw-sulcus')} plugin inside your ${cyan('openclaw.json')}.
367
+ Press ${bold('Enter')} to accept defaults. ${bold('Ctrl+C')} to cancel at any time.
368
+ `);
369
+
370
+ // ── Step 1: Locate openclaw.json ──────────────────────────────────────────
371
+
372
+ console.log(`${bold('Step 1 · Locate openclaw.json')}`);
373
+
374
+ let configPath = findOpenclawJson();
375
+
376
+ if (configPath) {
377
+ console.log(` ${green('✓')} Found: ${cyan(configPath)}\n`);
378
+ } else {
379
+ console.log(` ${yellow('⚠')} Could not find openclaw.json in the usual locations.`);
380
+ console.log(` Checked:`);
381
+ if (process.env.OPENCLAW_CONFIG_PATH)
382
+ console.log(` • \$OPENCLAW_CONFIG_PATH → ${process.env.OPENCLAW_CONFIG_PATH}`);
383
+ console.log(` • ~/.openclaw/openclaw.json`);
384
+ console.log(` • ./openclaw.json\n`);
385
+
386
+ const choice = await ask(
387
+ ` Enter full path to openclaw.json, or press Enter to create ~/.openclaw/openclaw.json:`,
388
+ path.join(os.homedir(), '.openclaw', 'openclaw.json'),
389
+ );
390
+ configPath = expandHome(choice);
391
+
392
+ if (!fs.existsSync(configPath)) {
393
+ const dir = path.dirname(configPath);
394
+ fs.mkdirSync(dir, { recursive: true });
395
+ fs.writeFileSync(configPath, JSON.stringify({}, null, 2), 'utf8');
396
+ console.log(` ${green('✓')} Created: ${cyan(configPath)}\n`);
397
+ } else {
398
+ console.log(` ${green('✓')} Using: ${cyan(configPath)}\n`);
399
+ }
400
+ }
401
+
402
+ // Read existing config
403
+ let existingConfig = {};
404
+ try {
405
+ existingConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
406
+ } catch (err) {
407
+ console.log(` ${red('✗')} Failed to parse openclaw.json: ${err.message}`);
408
+ console.log(` Fix the JSON syntax and re-run the wizard.\n`);
409
+ rl.close();
410
+ process.exit(1);
411
+ }
412
+
413
+ // ── Step 2: Wizard questions ──────────────────────────────────────────────
414
+
415
+ console.log(`${bold('Step 2 · Configure Sulcus')}`);
416
+ console.log();
417
+
418
+ // Backend mode
419
+ console.log(` ${bold('Backend mode:')}`);
420
+ console.log(` ${cyan('[1]')} Local only ${dim('(WASM + native dylibs, no network)')}`);
421
+ console.log(` ${cyan('[2]')} Cloud sync ${dim('(local + server replication)')}`);
422
+ const modeRaw = await ask(` >`, '1');
423
+ const cloudSync = modeRaw === '2';
424
+ console.log();
425
+
426
+ // Native dylib path
427
+ const libDirDefault = '~/.sulcus/lib';
428
+ const libDirRaw = await ask(
429
+ ` ${bold('Where are your native dylibs?')}`,
430
+ libDirDefault,
431
+ );
432
+ const libDir = libDirRaw;
433
+ console.log();
434
+
435
+ // Agent namespace
436
+ const namespace = await ask(` ${bold('Agent namespace:')}`, 'default');
437
+ console.log();
438
+
439
+ // Hooks
440
+ console.log(` ${bold('Enable hooks:')}`);
441
+ const injectAwareness = await askYN(
442
+ 'Inject memory awareness into prompts? (before_prompt_build)',
443
+ false,
444
+ );
445
+ const autoRecall = await askYN(
446
+ 'Auto-recall memories on each turn? (before_agent_start)',
447
+ false,
448
+ );
449
+ console.log();
450
+
451
+ // Tools
452
+ console.log(` ${bold('Enable tools:')}`);
453
+ const toolMemoryRecall = await askYN('memory_recall — search memories', true);
454
+ const toolMemoryStore = await askYN('memory_store — save memories', true);
455
+ const toolMemoryStatus = await askYN('memory_status — check memory stats', true);
456
+ const toolConsolidate = await askYN('consolidate — cluster similar memories', false);
457
+ const toolExportMarkdown = await askYN('export_markdown — export memories as markdown', false);
458
+ const toolImportMarkdown = await askYN('import_markdown — import from markdown', false);
459
+ const toolEvalTriggers = await askYN('evaluate_triggers — reactive trigger engine', false);
460
+ console.log();
461
+
462
+ // Cloud sync extras
463
+ let serverUrl = '';
464
+ let apiKey = '';
465
+ if (cloudSync) {
466
+ console.log(` ${bold('Cloud sync settings:')}`);
467
+ serverUrl = await ask(` Server URL:`, 'https://api.sulcus.ca');
468
+ apiKey = await ask(` API Key:`, '');
469
+ console.log();
470
+ }
471
+
472
+ // ── Step 3: Build and write config ───────────────────────────────────────
473
+
474
+ console.log(`${bold('Step 3 · Write openclaw.json')}`);
475
+
476
+ const sulcusConfig = {
477
+ libDir,
478
+ namespace: namespace === 'default' ? undefined : namespace,
479
+ ...(cloudSync && serverUrl ? { serverUrl } : {}),
480
+ ...(cloudSync && apiKey ? { apiKey } : {}),
481
+ hooks: {
482
+ before_prompt_build: { action: 'inject_awareness', enabled: injectAwareness },
483
+ before_agent_start: { action: 'auto_recall', enabled: autoRecall, limit: 5, minScore: 0.3 },
484
+ },
485
+ tools: {
486
+ memory_recall: { enabled: toolMemoryRecall },
487
+ memory_store: { enabled: toolMemoryStore },
488
+ memory_status: { enabled: toolMemoryStatus },
489
+ consolidate: { enabled: toolConsolidate },
490
+ export_markdown: { enabled: toolExportMarkdown },
491
+ import_markdown: { enabled: toolImportMarkdown },
492
+ evaluate_triggers: { enabled: toolEvalTriggers },
493
+ },
494
+ };
495
+
496
+ // Remove undefined keys
497
+ Object.keys(sulcusConfig).forEach(
498
+ (k) => sulcusConfig[k] === undefined && delete sulcusConfig[k],
499
+ );
500
+
501
+ // Deep-merge into existing config
502
+ const merged = deepMerge(existingConfig, {
503
+ plugins: {
504
+ entries: {
505
+ 'openclaw-sulcus': {
506
+ enabled: true,
507
+ config: sulcusConfig,
508
+ },
509
+ },
510
+ },
511
+ });
512
+
513
+ let written = false;
514
+ try {
515
+ fs.writeFileSync(configPath, JSON.stringify(merged, null, 2) + '\n', 'utf8');
516
+ written = true;
517
+ } catch (err) {
518
+ console.log(` ${red('✗')} Failed to write ${configPath}: ${err.message}\n`);
519
+ rl.close();
520
+ process.exit(1);
521
+ }
522
+
523
+ if (written) {
524
+ console.log(` ${green('✓')} Written to ${cyan(configPath)}`);
525
+ console.log();
526
+
527
+ // Summary
528
+ console.log(` ${dim('──── Summary ────────────────────────────────────')}`);
529
+ console.log(` Plugin: ${cyan('openclaw-sulcus')} ${green('enabled')}`);
530
+ console.log(` Backend: ${cyan(cloudSync ? 'cloud sync' : 'local only')}`);
531
+ console.log(` Dylib dir: ${cyan(libDir)}`);
532
+ console.log(` Namespace: ${cyan(namespace)}`);
533
+ if (cloudSync && serverUrl) console.log(` Server: ${cyan(serverUrl)}`);
534
+
535
+ const enabledHooks = [];
536
+ if (injectAwareness) enabledHooks.push('before_prompt_build');
537
+ if (autoRecall) enabledHooks.push('before_agent_start');
538
+ console.log(` Hooks: ${enabledHooks.length ? cyan(enabledHooks.join(', ')) : dim('(none enabled)')}`);
539
+
540
+ const enabledTools = [
541
+ toolMemoryRecall && 'memory_recall',
542
+ toolMemoryStore && 'memory_store',
543
+ toolMemoryStatus && 'memory_status',
544
+ toolConsolidate && 'consolidate',
545
+ toolExportMarkdown && 'export_markdown',
546
+ toolImportMarkdown && 'import_markdown',
547
+ toolEvalTriggers && 'evaluate_triggers',
548
+ ].filter(Boolean);
549
+ console.log(` Tools: ${enabledTools.length ? cyan(enabledTools.join(', ')) : dim('(none enabled)')}`);
550
+ console.log(` ${dim('─────────────────────────────────────────────────')}`);
551
+ console.log();
552
+ }
553
+
554
+ // ── Step 4: Validate dylib path (+ auto-download if missing) ─────────────
555
+
556
+ console.log(`${bold('Step 4 · Validate')}`);
557
+
558
+ const resolvedLibDir = expandHome(libDir);
559
+ const dylibNames = ['libsulcus_store', 'libsulcus_vectors'];
560
+ const ext = process.platform === 'darwin' ? '.dylib'
561
+ : process.platform === 'win32' ? '.dll'
562
+ : '.so';
563
+
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
+ }
571
+
572
+ let dylibsOk = checkDylibs();
573
+
574
+ if (dylibsOk) {
575
+ // All present — just print them
576
+ for (const lib of dylibNames) {
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.`);
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')}`);
607
+ }
608
+ }
609
+
610
+ if (dylibsOk) {
611
+ console.log(` ${green('✓')} All dylibs present — Sulcus is ready to go.`);
612
+ }
613
+
614
+ console.log();
615
+ console.log(` ${bold(green('✅ Configuration complete!'))} Restart the OpenClaw gateway to pick up changes:`);
616
+ console.log(` ${cyan('openclaw gateway restart')}`);
617
+ console.log();
618
+
619
+ rl.close();
620
+ }
621
+
622
+ run().catch((err) => {
623
+ console.error(`\n${red('Fatal error:')} ${err.message}\n`);
624
+ rl.close();
625
+ process.exit(1);
626
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@digitalforgestudios/openclaw-sulcus",
3
- "version": "3.2.2",
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",
@@ -37,7 +37,11 @@
37
37
  "openclawVersion": "2026.3.28"
38
38
  }
39
39
  },
40
+ "bin": {
41
+ "openclaw-sulcus": "./bin/configure.mjs"
42
+ },
40
43
  "files": [
44
+ "bin/",
41
45
  "index.ts",
42
46
  "wasm/",
43
47
  "openclaw.plugin.json",