@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.
- package/bin/configure.mjs +626 -0
- 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.
|
|
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",
|