@digitalforgestudios/openclaw-sulcus 3.2.1 → 3.3.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.
@@ -0,0 +1,399 @@
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
+
16
+ // ─── Colour support ───────────────────────────────────────────────────────────
17
+
18
+ const noColor =
19
+ process.argv.includes('--no-color') ||
20
+ process.env.NO_COLOR !== undefined ||
21
+ !process.stdout.isTTY;
22
+
23
+ const c = {
24
+ reset: noColor ? '' : '\x1b[0m',
25
+ bold: noColor ? '' : '\x1b[1m',
26
+ dim: noColor ? '' : '\x1b[2m',
27
+ red: noColor ? '' : '\x1b[31m',
28
+ green: noColor ? '' : '\x1b[32m',
29
+ yellow: noColor ? '' : '\x1b[33m',
30
+ blue: noColor ? '' : '\x1b[34m',
31
+ magenta: noColor ? '' : '\x1b[35m',
32
+ cyan: noColor ? '' : '\x1b[36m',
33
+ white: noColor ? '' : '\x1b[37m',
34
+ };
35
+
36
+ const bold = (s) => `${c.bold}${s}${c.reset}`;
37
+ const dim = (s) => `${c.dim}${s}${c.reset}`;
38
+ const green = (s) => `${c.green}${s}${c.reset}`;
39
+ const yellow = (s) => `${c.yellow}${s}${c.reset}`;
40
+ const red = (s) => `${c.red}${s}${c.reset}`;
41
+ const cyan = (s) => `${c.cyan}${s}${c.reset}`;
42
+ const magenta = (s) => `${c.magenta}${s}${c.reset}`;
43
+
44
+ // ─── Help ─────────────────────────────────────────────────────────────────────
45
+
46
+ if (process.argv.includes('--help') || process.argv.includes('-h')) {
47
+ console.log(`
48
+ ${bold('Sulcus Configuration Wizard')}
49
+
50
+ Interactively configure the openclaw-sulcus plugin inside your openclaw.json.
51
+
52
+ ${bold('Usage:')}
53
+ npx @digitalforgestudios/openclaw-sulcus configure [options]
54
+ node bin/configure.mjs [options]
55
+
56
+ ${bold('Options:')}
57
+ --help, -h Show this help message
58
+ --no-color Disable coloured output
59
+
60
+ ${bold('What it does:')}
61
+ 1. Locates your openclaw.json (checks \$OPENCLAW_CONFIG_PATH, ~/.openclaw/, ./)
62
+ 2. Walks you through backend mode, dylib path, namespace, hooks, and tools
63
+ 3. Deep-merges settings under plugins.entries.openclaw-sulcus.config
64
+ 4. Validates that your native dylibs exist and warns if they are missing
65
+ 5. Reminds you to restart the OpenClaw gateway
66
+
67
+ ${bold('Example:')}
68
+ npx @digitalforgestudios/openclaw-sulcus configure
69
+ `);
70
+ process.exit(0);
71
+ }
72
+
73
+ // ─── Readline helpers ─────────────────────────────────────────────────────────
74
+
75
+ const rl = readline.createInterface({
76
+ input: process.stdin,
77
+ output: process.stdout,
78
+ });
79
+
80
+ // Graceful Ctrl+C
81
+ rl.on('SIGINT', () => {
82
+ console.log(`\n\n${yellow('⚡ Wizard cancelled — no changes were written.')}\n`);
83
+ process.exit(0);
84
+ });
85
+
86
+ /**
87
+ * Prompt the user with an optional default value.
88
+ * Returns the trimmed answer, or the default if empty.
89
+ */
90
+ function ask(question, defaultValue = '') {
91
+ return new Promise((resolve) => {
92
+ const hint = defaultValue !== '' ? dim(` [${defaultValue}]`) : '';
93
+ rl.question(`${question}${hint} `, (answer) => {
94
+ const trimmed = answer.trim();
95
+ resolve(trimmed === '' ? defaultValue : trimmed);
96
+ });
97
+ });
98
+ }
99
+
100
+ /**
101
+ * Ask a yes/no question. Returns boolean.
102
+ * @param {string} question
103
+ * @param {boolean} defaultVal
104
+ */
105
+ function askYN(question, defaultVal = false) {
106
+ return new Promise((resolve) => {
107
+ const hint = dim(` [${defaultVal ? 'Y/n' : 'y/N'}]`);
108
+ rl.question(` ${question}${hint} `, (answer) => {
109
+ const a = answer.trim().toLowerCase();
110
+ if (a === '') resolve(defaultVal);
111
+ else resolve(a === 'y' || a === 'yes');
112
+ });
113
+ });
114
+ }
115
+
116
+ // ─── openclaw.json discovery ──────────────────────────────────────────────────
117
+
118
+ function expandHome(p) {
119
+ if (p.startsWith('~')) return path.join(os.homedir(), p.slice(1));
120
+ return p;
121
+ }
122
+
123
+ function findOpenclawJson() {
124
+ const candidates = [
125
+ process.env.OPENCLAW_CONFIG_PATH,
126
+ path.join(os.homedir(), '.openclaw', 'openclaw.json'),
127
+ path.join(process.cwd(), 'openclaw.json'),
128
+ ].filter(Boolean);
129
+
130
+ for (const candidate of candidates) {
131
+ const resolved = expandHome(candidate);
132
+ if (fs.existsSync(resolved)) return resolved;
133
+ }
134
+ return null;
135
+ }
136
+
137
+ // Deep-merge two plain objects (target mutated).
138
+ function deepMerge(target, source) {
139
+ for (const key of Object.keys(source)) {
140
+ if (
141
+ source[key] !== null &&
142
+ typeof source[key] === 'object' &&
143
+ !Array.isArray(source[key]) &&
144
+ typeof target[key] === 'object' &&
145
+ target[key] !== null &&
146
+ !Array.isArray(target[key])
147
+ ) {
148
+ deepMerge(target[key], source[key]);
149
+ } else {
150
+ target[key] = source[key];
151
+ }
152
+ }
153
+ return target;
154
+ }
155
+
156
+ // ─── Main wizard ──────────────────────────────────────────────────────────────
157
+
158
+ async function run() {
159
+ console.log(`
160
+ ${bold(magenta('🧠 Sulcus Configuration Wizard'))}
161
+ ${dim('────────────────────────────────────────────')}
162
+ Configures the ${cyan('openclaw-sulcus')} plugin inside your ${cyan('openclaw.json')}.
163
+ Press ${bold('Enter')} to accept defaults. ${bold('Ctrl+C')} to cancel at any time.
164
+ `);
165
+
166
+ // ── Step 1: Locate openclaw.json ──────────────────────────────────────────
167
+
168
+ console.log(`${bold('Step 1 · Locate openclaw.json')}`);
169
+
170
+ let configPath = findOpenclawJson();
171
+
172
+ if (configPath) {
173
+ console.log(` ${green('✓')} Found: ${cyan(configPath)}\n`);
174
+ } else {
175
+ console.log(` ${yellow('⚠')} Could not find openclaw.json in the usual locations.`);
176
+ console.log(` Checked:`);
177
+ if (process.env.OPENCLAW_CONFIG_PATH)
178
+ console.log(` • \$OPENCLAW_CONFIG_PATH → ${process.env.OPENCLAW_CONFIG_PATH}`);
179
+ console.log(` • ~/.openclaw/openclaw.json`);
180
+ console.log(` • ./openclaw.json\n`);
181
+
182
+ const choice = await ask(
183
+ ` Enter full path to openclaw.json, or press Enter to create ~/.openclaw/openclaw.json:`,
184
+ path.join(os.homedir(), '.openclaw', 'openclaw.json'),
185
+ );
186
+ configPath = expandHome(choice);
187
+
188
+ if (!fs.existsSync(configPath)) {
189
+ const dir = path.dirname(configPath);
190
+ fs.mkdirSync(dir, { recursive: true });
191
+ fs.writeFileSync(configPath, JSON.stringify({}, null, 2), 'utf8');
192
+ console.log(` ${green('✓')} Created: ${cyan(configPath)}\n`);
193
+ } else {
194
+ console.log(` ${green('✓')} Using: ${cyan(configPath)}\n`);
195
+ }
196
+ }
197
+
198
+ // Read existing config
199
+ let existingConfig = {};
200
+ try {
201
+ existingConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
202
+ } catch (err) {
203
+ console.log(` ${red('✗')} Failed to parse openclaw.json: ${err.message}`);
204
+ console.log(` Fix the JSON syntax and re-run the wizard.\n`);
205
+ rl.close();
206
+ process.exit(1);
207
+ }
208
+
209
+ // ── Step 2: Wizard questions ──────────────────────────────────────────────
210
+
211
+ console.log(`${bold('Step 2 · Configure Sulcus')}`);
212
+ console.log();
213
+
214
+ // Backend mode
215
+ console.log(` ${bold('Backend mode:')}`);
216
+ console.log(` ${cyan('[1]')} Local only ${dim('(WASM + native dylibs, no network)')}`);
217
+ console.log(` ${cyan('[2]')} Cloud sync ${dim('(local + server replication)')}`);
218
+ const modeRaw = await ask(` >`, '1');
219
+ const cloudSync = modeRaw === '2';
220
+ console.log();
221
+
222
+ // Native dylib path
223
+ const libDirDefault = '~/.sulcus/lib';
224
+ const libDirRaw = await ask(
225
+ ` ${bold('Where are your native dylibs?')}`,
226
+ libDirDefault,
227
+ );
228
+ const libDir = libDirRaw;
229
+ console.log();
230
+
231
+ // Agent namespace
232
+ const namespace = await ask(` ${bold('Agent namespace:')}`, 'default');
233
+ console.log();
234
+
235
+ // Hooks
236
+ console.log(` ${bold('Enable hooks:')}`);
237
+ const injectAwareness = await askYN(
238
+ 'Inject memory awareness into prompts? (before_prompt_build)',
239
+ false,
240
+ );
241
+ const autoRecall = await askYN(
242
+ 'Auto-recall memories on each turn? (before_agent_start)',
243
+ false,
244
+ );
245
+ console.log();
246
+
247
+ // Tools
248
+ console.log(` ${bold('Enable tools:')}`);
249
+ const toolMemoryRecall = await askYN('memory_recall — search memories', true);
250
+ const toolMemoryStore = await askYN('memory_store — save memories', true);
251
+ const toolMemoryStatus = await askYN('memory_status — check memory stats', true);
252
+ const toolConsolidate = await askYN('consolidate — cluster similar memories', false);
253
+ const toolExportMarkdown = await askYN('export_markdown — export memories as markdown', false);
254
+ const toolImportMarkdown = await askYN('import_markdown — import from markdown', false);
255
+ const toolEvalTriggers = await askYN('evaluate_triggers — reactive trigger engine', false);
256
+ console.log();
257
+
258
+ // Cloud sync extras
259
+ let serverUrl = '';
260
+ let apiKey = '';
261
+ if (cloudSync) {
262
+ console.log(` ${bold('Cloud sync settings:')}`);
263
+ serverUrl = await ask(` Server URL:`, 'https://api.sulcus.ca');
264
+ apiKey = await ask(` API Key:`, '');
265
+ console.log();
266
+ }
267
+
268
+ // ── Step 3: Build and write config ───────────────────────────────────────
269
+
270
+ console.log(`${bold('Step 3 · Write openclaw.json')}`);
271
+
272
+ const sulcusConfig = {
273
+ libDir,
274
+ namespace: namespace === 'default' ? undefined : namespace,
275
+ ...(cloudSync && serverUrl ? { serverUrl } : {}),
276
+ ...(cloudSync && apiKey ? { apiKey } : {}),
277
+ hooks: {
278
+ before_prompt_build: { action: 'inject_awareness', enabled: injectAwareness },
279
+ before_agent_start: { action: 'auto_recall', enabled: autoRecall, limit: 5, minScore: 0.3 },
280
+ },
281
+ tools: {
282
+ memory_recall: { enabled: toolMemoryRecall },
283
+ memory_store: { enabled: toolMemoryStore },
284
+ memory_status: { enabled: toolMemoryStatus },
285
+ consolidate: { enabled: toolConsolidate },
286
+ export_markdown: { enabled: toolExportMarkdown },
287
+ import_markdown: { enabled: toolImportMarkdown },
288
+ evaluate_triggers: { enabled: toolEvalTriggers },
289
+ },
290
+ };
291
+
292
+ // Remove undefined keys
293
+ Object.keys(sulcusConfig).forEach(
294
+ (k) => sulcusConfig[k] === undefined && delete sulcusConfig[k],
295
+ );
296
+
297
+ // Deep-merge into existing config
298
+ const merged = deepMerge(existingConfig, {
299
+ plugins: {
300
+ entries: {
301
+ 'openclaw-sulcus': {
302
+ enabled: true,
303
+ config: sulcusConfig,
304
+ },
305
+ },
306
+ },
307
+ });
308
+
309
+ let written = false;
310
+ try {
311
+ fs.writeFileSync(configPath, JSON.stringify(merged, null, 2) + '\n', 'utf8');
312
+ written = true;
313
+ } catch (err) {
314
+ console.log(` ${red('✗')} Failed to write ${configPath}: ${err.message}\n`);
315
+ rl.close();
316
+ process.exit(1);
317
+ }
318
+
319
+ if (written) {
320
+ console.log(` ${green('✓')} Written to ${cyan(configPath)}`);
321
+ console.log();
322
+
323
+ // Summary
324
+ console.log(` ${dim('──── Summary ────────────────────────────────────')}`);
325
+ console.log(` Plugin: ${cyan('openclaw-sulcus')} ${green('enabled')}`);
326
+ console.log(` Backend: ${cyan(cloudSync ? 'cloud sync' : 'local only')}`);
327
+ console.log(` Dylib dir: ${cyan(libDir)}`);
328
+ console.log(` Namespace: ${cyan(namespace)}`);
329
+ if (cloudSync && serverUrl) console.log(` Server: ${cyan(serverUrl)}`);
330
+
331
+ const enabledHooks = [];
332
+ if (injectAwareness) enabledHooks.push('before_prompt_build');
333
+ if (autoRecall) enabledHooks.push('before_agent_start');
334
+ console.log(` Hooks: ${enabledHooks.length ? cyan(enabledHooks.join(', ')) : dim('(none enabled)')}`);
335
+
336
+ const enabledTools = [
337
+ toolMemoryRecall && 'memory_recall',
338
+ toolMemoryStore && 'memory_store',
339
+ toolMemoryStatus && 'memory_status',
340
+ toolConsolidate && 'consolidate',
341
+ toolExportMarkdown && 'export_markdown',
342
+ toolImportMarkdown && 'import_markdown',
343
+ toolEvalTriggers && 'evaluate_triggers',
344
+ ].filter(Boolean);
345
+ console.log(` Tools: ${enabledTools.length ? cyan(enabledTools.join(', ')) : dim('(none enabled)')}`);
346
+ console.log(` ${dim('─────────────────────────────────────────────────')}`);
347
+ console.log();
348
+ }
349
+
350
+ // ── Step 4: Validate dylib path ───────────────────────────────────────────
351
+
352
+ console.log(`${bold('Step 4 · Validate')}`);
353
+
354
+ const resolvedLibDir = expandHome(libDir);
355
+ const dylibNames = ['libsulcus_store', 'libsulcus_vectors'];
356
+ const ext = process.platform === 'darwin' ? '.dylib'
357
+ : process.platform === 'win32' ? '.dll'
358
+ : '.so';
359
+
360
+ let dylibsOk = true;
361
+
362
+ if (!fs.existsSync(resolvedLibDir)) {
363
+ dylibsOk = false;
364
+ console.log(` ${yellow('⚠')} Dylib directory not found: ${cyan(resolvedLibDir)}`);
365
+ } else {
366
+ 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)}`);
373
+ }
374
+ }
375
+ }
376
+
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 {
384
+ console.log(` ${green('✓')} All dylibs present — Sulcus is ready to go.`);
385
+ }
386
+
387
+ console.log();
388
+ console.log(` ${bold(green('✅ Configuration complete!'))} Restart the OpenClaw gateway to pick up changes:`);
389
+ console.log(` ${cyan('openclaw gateway restart')}`);
390
+ console.log();
391
+
392
+ rl.close();
393
+ }
394
+
395
+ run().catch((err) => {
396
+ console.error(`\n${red('Fatal error:')} ${err.message}\n`);
397
+ rl.close();
398
+ process.exit(1);
399
+ });
@@ -4,7 +4,7 @@
4
4
  "hooks": {
5
5
  "before_prompt_build": {
6
6
  "action": "inject_awareness",
7
- "enabled": true
7
+ "enabled": false
8
8
  },
9
9
  "before_agent_start": {
10
10
  "action": "auto_recall",
@@ -14,13 +14,13 @@
14
14
  },
15
15
  "agent_end": {
16
16
  "action": "none",
17
- "enabled": true
17
+ "enabled": false
18
18
  }
19
19
  },
20
20
  "tools": {
21
- "memory_recall": { "enabled": true },
22
- "memory_store": { "enabled": true },
23
- "memory_status": { "enabled": true },
21
+ "memory_recall": { "enabled": false },
22
+ "memory_store": { "enabled": false },
23
+ "memory_status": { "enabled": false },
24
24
  "consolidate": { "enabled": false },
25
25
  "export_markdown": { "enabled": false },
26
26
  "import_markdown": { "enabled": false },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@digitalforgestudios/openclaw-sulcus",
3
- "version": "3.2.1",
3
+ "version": "3.3.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",
@@ -277,8 +277,13 @@ function __wbg_get_imports() {
277
277
  return ret;
278
278
  },
279
279
  __wbg_new_no_args_1c7c842f08d00ebb: function(arg0, arg1) {
280
- const ret = new Function(getStringFromWasm0(arg0, arg1));
281
- return ret;
280
+ // Avoid dynamic code execution (new Function) — use safe globalThis fallback
281
+ const body = getStringFromWasm0(arg0, arg1);
282
+ if (body.includes('return this')) {
283
+ const ret = { call: () => globalThis };
284
+ return ret;
285
+ }
286
+ throw new Error(`sulcus-wasm: refusing dynamic code execution: ${body.substring(0, 60)}`);
282
287
  },
283
288
  __wbg_parse_708461a1feddfb38: function() { return handleError(function (arg0, arg1) {
284
289
  const ret = JSON.parse(getStringFromWasm0(arg0, arg1));