@digitalforgestudios/openclaw-sulcus 3.5.5 → 3.6.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 +502 -72
  2. package/package.json +1 -1
package/bin/configure.mjs CHANGED
@@ -1,10 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * Sulcus Configuration Wizard
4
- * Interactive CLI to configure the openclaw-sulcus plugin in openclaw.json
4
+ * Interactive CLI to configure Sulcus for multiple AI tools.
5
5
  *
6
6
  * Usage:
7
- * npx @digitalforgestudios/openclaw-sulcus configure
7
+ * npx @digitalforgestudios/openclaw-sulcus configure # OpenClaw
8
+ * npx @digitalforgestudios/openclaw-sulcus configure --claude # Claude CLI / Claude Code
9
+ * npx @digitalforgestudios/openclaw-sulcus configure --openai # OpenAI Codex CLI
10
+ * npx @digitalforgestudios/openclaw-sulcus configure --gemini # Google Gemini CLI
8
11
  * node bin/configure.mjs [--no-color] [--help]
9
12
  */
10
13
 
@@ -43,31 +46,60 @@ const red = (s) => `${c.red}${s}${c.reset}`;
43
46
  const cyan = (s) => `${c.cyan}${s}${c.reset}`;
44
47
  const magenta = (s) => `${c.magenta}${s}${c.reset}`;
45
48
 
49
+ // ─── Mode detection ───────────────────────────────────────────────────────────
50
+
51
+ const MODE_OPENCLAW = 'openclaw';
52
+ const MODE_CLAUDE = 'claude';
53
+ const MODE_OPENAI = 'openai';
54
+ const MODE_GEMINI = 'gemini';
55
+
56
+ function detectMode() {
57
+ if (process.argv.includes('--claude')) return MODE_CLAUDE;
58
+ if (process.argv.includes('--openai')) return MODE_OPENAI;
59
+ if (process.argv.includes('--gemini')) return MODE_GEMINI;
60
+ return MODE_OPENCLAW;
61
+ }
62
+
46
63
  // ─── Help ─────────────────────────────────────────────────────────────────────
47
64
 
48
65
  if (process.argv.includes('--help') || process.argv.includes('-h')) {
49
66
  console.log(`
50
67
  ${bold('Sulcus Configuration Wizard')}
51
68
 
52
- Interactively configure the openclaw-sulcus plugin inside your openclaw.json.
69
+ Interactively configure Sulcus as an MCP server for your AI tools.
53
70
 
54
71
  ${bold('Usage:')}
55
- npx @digitalforgestudios/openclaw-sulcus configure [options]
56
- node bin/configure.mjs [options]
72
+ npx @digitalforgestudios/openclaw-sulcus configure [mode] [options]
73
+ node bin/configure.mjs [mode] [options]
74
+
75
+ ${bold('Modes:')}
76
+ ${dim('(no flag)')} Configure for ${cyan('OpenClaw')} (openclaw.json)
77
+ ${cyan('--claude')} Configure for ${cyan('Claude CLI / Claude Code')} (~/.claude/claude_desktop_config.json)
78
+ ${cyan('--openai')} Configure for ${cyan('OpenAI Codex CLI')} (~/.codex/config.toml)
79
+ ${cyan('--gemini')} Configure for ${cyan('Google Gemini CLI')} (~/.gemini/settings.json)
57
80
 
58
81
  ${bold('Options:')}
59
82
  --help, -h Show this help message
60
83
  --no-color Disable coloured output
61
84
 
62
- ${bold('What it does:')}
63
- 1. Locates your openclaw.json (checks \$OPENCLAW_CONFIG_PATH, ~/.openclaw/, ./)
85
+ ${bold('OpenClaw mode (default):')}
86
+ 1. Locates your openclaw.json (checks $OPENCLAW_CONFIG_PATH, ~/.openclaw/, ./)
64
87
  2. Walks you through backend mode, dylib path, namespace, hooks, and tools
65
88
  3. Deep-merges settings under plugins.entries.openclaw-sulcus.config
66
89
  4. Validates that your native dylibs exist and warns if they are missing
67
90
  5. Reminds you to restart the OpenClaw gateway
68
91
 
69
- ${bold('Example:')}
92
+ ${bold('MCP server modes (--claude / --openai / --gemini):')}
93
+ 1. Detects if the target CLI is installed
94
+ 2. Asks: Cloud mode (Sulcus API) or Local mode (binary path)?
95
+ 3. Merges the sulcus MCP server entry into the target config
96
+ 4. Preserves all existing config settings
97
+
98
+ ${bold('Examples:')}
70
99
  npx @digitalforgestudios/openclaw-sulcus configure
100
+ npx @digitalforgestudios/openclaw-sulcus configure --claude
101
+ npx @digitalforgestudios/openclaw-sulcus configure --openai
102
+ npx @digitalforgestudios/openclaw-sulcus configure --gemini
71
103
  `);
72
104
  process.exit(0);
73
105
  }
@@ -101,8 +133,6 @@ function ask(question, defaultValue = '') {
101
133
 
102
134
  /**
103
135
  * Ask a yes/no question. Returns boolean.
104
- * @param {string} question
105
- * @param {boolean} defaultVal
106
136
  */
107
137
  function askYN(question, defaultVal = false) {
108
138
  return new Promise((resolve) => {
@@ -115,27 +145,13 @@ function askYN(question, defaultVal = false) {
115
145
  });
116
146
  }
117
147
 
118
- // ─── openclaw.json discovery ──────────────────────────────────────────────────
148
+ // ─── Utility helpers ──────────────────────────────────────────────────────────
119
149
 
120
150
  function expandHome(p) {
121
151
  if (p.startsWith('~')) return path.join(os.homedir(), p.slice(1));
122
152
  return p;
123
153
  }
124
154
 
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
155
  // Deep-merge two plain objects (target mutated).
140
156
  function deepMerge(target, source) {
141
157
  for (const key of Object.keys(source)) {
@@ -155,12 +171,448 @@ function deepMerge(target, source) {
155
171
  return target;
156
172
  }
157
173
 
158
- // ─── Prebuilt binary download ─────────────────────────────────────────────────
174
+ // ─── Binary detection (sulcus executable) ────────────────────────────────────
175
+
176
+ const SULCUS_BINARY_SEARCH_PATHS = [
177
+ path.join(os.homedir(), '.sulcus', 'bin', 'sulcus'),
178
+ path.join(os.homedir(), '.local', 'bin', 'sulcus'),
179
+ '/usr/local/bin/sulcus',
180
+ ];
159
181
 
160
182
  /**
161
- * Detect the current platform slug used in GitHub release asset names.
162
- * Returns { platform, ext } or throws if unsupported.
183
+ * Try to locate the sulcus binary.
184
+ * Returns the absolute path if found, or null.
163
185
  */
186
+ function findSulcusBinary() {
187
+ for (const p of SULCUS_BINARY_SEARCH_PATHS) {
188
+ if (fs.existsSync(p)) return p;
189
+ }
190
+ try {
191
+ const result = execSync('which sulcus', { stdio: 'pipe' }).toString().trim();
192
+ if (result && fs.existsSync(result)) return result;
193
+ } catch (_) {
194
+ // not on PATH
195
+ }
196
+ return null;
197
+ }
198
+
199
+ /**
200
+ * Check if a CLI tool is installed by running `which <name>`.
201
+ */
202
+ function isCliInstalled(name) {
203
+ try {
204
+ execSync(`which ${name}`, { stdio: 'pipe' });
205
+ return true;
206
+ } catch (_) {
207
+ return false;
208
+ }
209
+ }
210
+
211
+ // ─── JSON config helpers ──────────────────────────────────────────────────────
212
+
213
+ function readJsonConfig(filePath) {
214
+ try {
215
+ if (!fs.existsSync(filePath)) return {};
216
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
217
+ } catch (_) {
218
+ return {};
219
+ }
220
+ }
221
+
222
+ function writeJsonConfig(filePath, data) {
223
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
224
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8');
225
+ }
226
+
227
+ // ─── Minimal hand-rolled TOML helpers ────────────────────────────────────────
228
+ // Supports the subset needed for ~/.codex/config.toml:
229
+ // - Bare/quoted string values, integers, booleans
230
+ // - Arrays of quoted strings: key = ["a", "b"]
231
+ // - [table] and [table.subtable] headers
232
+ // - # line comments
233
+
234
+ function parseToml(src) {
235
+ const lines = src.split('\n');
236
+ const root = {};
237
+ let current = root;
238
+
239
+ for (let rawLine of lines) {
240
+ const line = rawLine.trim();
241
+ if (!line || line.startsWith('#')) continue;
242
+
243
+ // Table header: [a.b.c]
244
+ const tableMatch = line.match(/^\[([^\]]+)\]$/);
245
+ if (tableMatch) {
246
+ const parts = tableMatch[1].trim().split('.').map(s => s.trim());
247
+ current = root;
248
+ for (const part of parts) {
249
+ if (current[part] === undefined) current[part] = {};
250
+ else if (typeof current[part] !== 'object' || Array.isArray(current[part])) {
251
+ current[part] = {};
252
+ }
253
+ current = current[part];
254
+ }
255
+ continue;
256
+ }
257
+
258
+ const eqIdx = line.indexOf('=');
259
+ if (eqIdx === -1) continue;
260
+
261
+ const key = line.slice(0, eqIdx).trim();
262
+ let valStr = line.slice(eqIdx + 1).trim();
263
+ let value;
264
+
265
+ if (valStr.startsWith('[')) {
266
+ // Array of strings
267
+ const inner = valStr.slice(1, valStr.lastIndexOf(']'));
268
+ value = inner
269
+ .split(',')
270
+ .map(s => s.trim().replace(/^["']|["']$/g, ''))
271
+ .filter(s => s.length > 0);
272
+ } else if (valStr.startsWith('"') || valStr.startsWith("'")) {
273
+ const q = valStr[0];
274
+ const end = valStr.indexOf(q, 1);
275
+ value = end === -1 ? valStr.slice(1) : valStr.slice(1, end);
276
+ } else if (valStr === 'true') {
277
+ value = true;
278
+ } else if (valStr === 'false') {
279
+ value = false;
280
+ } else {
281
+ const commentIdx = valStr.indexOf('#');
282
+ if (commentIdx !== -1) valStr = valStr.slice(0, commentIdx).trim();
283
+ const num = Number(valStr);
284
+ value = isNaN(num) || valStr === '' ? valStr : num;
285
+ }
286
+
287
+ current[key] = value;
288
+ }
289
+
290
+ return root;
291
+ }
292
+
293
+ function serializeToml(obj, prefix = []) {
294
+ const scalarLines = [];
295
+ const subTables = [];
296
+
297
+ for (const [key, val] of Object.entries(obj)) {
298
+ if (val === null || val === undefined) continue;
299
+
300
+ if (Array.isArray(val)) {
301
+ const items = val.map(v => JSON.stringify(String(v))).join(', ');
302
+ scalarLines.push(`${key} = [${items}]`);
303
+ } else if (typeof val === 'object') {
304
+ subTables.push([key, val]);
305
+ } else if (typeof val === 'string') {
306
+ scalarLines.push(`${key} = ${JSON.stringify(val)}`);
307
+ } else {
308
+ scalarLines.push(`${key} = ${val}`);
309
+ }
310
+ }
311
+
312
+ const parts = [...scalarLines];
313
+
314
+ for (const [key, val] of subTables) {
315
+ const tablePath = [...prefix, key];
316
+ parts.push('');
317
+ parts.push(`[${tablePath.join('.')}]`);
318
+ const inner = serializeToml(val, tablePath);
319
+ if (inner) parts.push(inner);
320
+ }
321
+
322
+ return parts.join('\n');
323
+ }
324
+
325
+ function readTomlConfig(filePath) {
326
+ try {
327
+ if (!fs.existsSync(filePath)) return {};
328
+ return parseToml(fs.readFileSync(filePath, 'utf8'));
329
+ } catch (_) {
330
+ return {};
331
+ }
332
+ }
333
+
334
+ function writeTomlConfig(filePath, data) {
335
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
336
+ const content = serializeToml(data).trimStart() + '\n';
337
+ fs.writeFileSync(filePath, content, 'utf8');
338
+ }
339
+
340
+ // ─── Shared MCP server config prompts ────────────────────────────────────────
341
+
342
+ /**
343
+ * Ask cloud vs local, collect info, return an MCP server config object:
344
+ * { command, args, env? }
345
+ */
346
+ async function askMcpServerConfig() {
347
+ console.log();
348
+ console.log(` ${bold('Mode:')}`);
349
+ console.log(` ${cyan('[1]')} Cloud ${dim('(Sulcus API — requires server URL + API key)')}`);
350
+ console.log(` ${cyan('[2]')} Local ${dim('(sulcus binary installed on this machine)')}`);
351
+ const modeRaw = await ask(` >`, '1');
352
+ const isCloud = modeRaw !== '2';
353
+ console.log();
354
+
355
+ if (isCloud) {
356
+ const serverUrl = await ask(
357
+ ` ${bold('Sulcus server URL:')}`,
358
+ 'https://api.sulcus.ca',
359
+ );
360
+ const apiKey = await ask(` ${bold('Sulcus API key')} ${dim('(sk-...)')}:`, '');
361
+ console.log();
362
+
363
+ const env = {};
364
+ if (serverUrl) env.SULCUS_SERVER_URL = serverUrl;
365
+ if (apiKey) env.SULCUS_API_KEY = apiKey;
366
+
367
+ return {
368
+ command: expandHome('~/.sulcus/bin/sulcus'),
369
+ args: ['stdio'],
370
+ ...(Object.keys(env).length > 0 ? { env } : {}),
371
+ };
372
+ } else {
373
+ // Local mode — detect binary
374
+ const detected = findSulcusBinary();
375
+ if (detected) {
376
+ console.log(` ${green('✓')} Found sulcus binary: ${cyan(detected)}`);
377
+ } else {
378
+ console.log(` ${yellow('⚠')} sulcus binary not found in common locations.`);
379
+ console.log(` ${dim('Search paths checked:')}`);
380
+ for (const p of SULCUS_BINARY_SEARCH_PATHS) {
381
+ console.log(` ${dim('•')} ${dim(p)}`);
382
+ }
383
+ console.log(` ${dim('Download from:')} ${cyan('https://github.com/digitalforgeca/sulcus/releases/latest')}`);
384
+ console.log();
385
+ }
386
+
387
+ const binaryPath = await ask(
388
+ ` ${bold('Path to sulcus binary:')}`,
389
+ detected || expandHome('~/.sulcus/bin/sulcus'),
390
+ );
391
+ console.log();
392
+
393
+ const wantCloudEnv = await askYN(
394
+ 'Also add SULCUS_SERVER_URL / SULCUS_API_KEY? (for cloud sync)',
395
+ false,
396
+ );
397
+ console.log();
398
+
399
+ let env;
400
+ if (wantCloudEnv) {
401
+ const serverUrl = await ask(
402
+ ` ${bold('Sulcus server URL:')}`,
403
+ 'https://api.sulcus.ca',
404
+ );
405
+ const apiKey = await ask(` ${bold('Sulcus API key')} ${dim('(sk-...)')}:`, '');
406
+ console.log();
407
+ const e = {};
408
+ if (serverUrl) e.SULCUS_SERVER_URL = serverUrl;
409
+ if (apiKey) e.SULCUS_API_KEY = apiKey;
410
+ if (Object.keys(e).length > 0) env = e;
411
+ }
412
+
413
+ return {
414
+ command: binaryPath,
415
+ args: ['stdio'],
416
+ ...(env ? { env } : {}),
417
+ };
418
+ }
419
+ }
420
+
421
+ /**
422
+ * Print an MCP server summary block.
423
+ */
424
+ function printMcpSummary(toolName, configFile, serverCfg) {
425
+ const displayFile = configFile.replace(os.homedir(), '~');
426
+ console.log(` ${dim('──── Summary ────────────────────────────────────────')}`);
427
+ console.log(` Tool: ${cyan(toolName)}`);
428
+ console.log(` Config: ${cyan(displayFile)}`);
429
+ console.log(` MCP server: ${cyan('sulcus')}`);
430
+ console.log(` Command: ${cyan(serverCfg.command)}`);
431
+ console.log(` Args: ${cyan(serverCfg.args.join(', '))}`);
432
+ if (serverCfg.env && Object.keys(serverCfg.env).length > 0) {
433
+ for (const [k, v] of Object.entries(serverCfg.env)) {
434
+ const display = k === 'SULCUS_API_KEY' && v && v.length > 8
435
+ ? v.slice(0, 8) + '...'
436
+ : v;
437
+ console.log(` ${k}: ${cyan(display)}`);
438
+ }
439
+ }
440
+ console.log(` ${dim('─────────────────────────────────────────────────────')}`);
441
+ console.log();
442
+ }
443
+
444
+ // ─── Claude wizard ────────────────────────────────────────────────────────────
445
+
446
+ async function runClaudeWizard() {
447
+ const configFile = expandHome('~/.claude/claude_desktop_config.json');
448
+
449
+ console.log(`
450
+ ${bold(magenta('🧠 Sulcus → Claude CLI Configuration Wizard'))}
451
+ ${dim('──────────────────────────────────────────────────────')}
452
+ Configures Sulcus as an MCP server for ${cyan('Claude CLI / Claude Code')}.
453
+ Config file: ${cyan('~/.claude/claude_desktop_config.json')}
454
+ Press ${bold('Enter')} to accept defaults. ${bold('Ctrl+C')} to cancel at any time.
455
+ `);
456
+
457
+ // Detect claude CLI
458
+ const claudeInstalled = isCliInstalled('claude');
459
+ if (claudeInstalled) {
460
+ console.log(` ${green('✓')} claude CLI detected on PATH`);
461
+ } else {
462
+ console.log(` ${yellow('⚠')} claude CLI not found on PATH`);
463
+ console.log(` ${dim('Install Claude Code from:')} ${cyan('https://claude.ai/download')}`);
464
+ }
465
+
466
+ const serverCfg = await askMcpServerConfig();
467
+
468
+ console.log(`${bold('Writing config...')}`);
469
+
470
+ let existing = readJsonConfig(configFile);
471
+ if (!existing.mcpServers) existing.mcpServers = {};
472
+ existing.mcpServers.sulcus = serverCfg;
473
+
474
+ try {
475
+ writeJsonConfig(configFile, existing);
476
+ } catch (err) {
477
+ console.log(` ${red('✗')} Failed to write ${cyan(configFile)}: ${err.message}`);
478
+ rl.close();
479
+ process.exit(1);
480
+ }
481
+
482
+ console.log(` ${green('✓')} Written to ${cyan(configFile.replace(os.homedir(), '~'))}`);
483
+ console.log();
484
+
485
+ printMcpSummary('Claude CLI / Claude Code', configFile, serverCfg);
486
+
487
+ console.log(` ${bold(green('✅ Configuration complete!'))} Restart Claude to pick up changes.`);
488
+ console.log();
489
+
490
+ rl.close();
491
+ }
492
+
493
+ // ─── OpenAI Codex wizard ──────────────────────────────────────────────────────
494
+
495
+ async function runOpenAIWizard() {
496
+ const configFile = expandHome('~/.codex/config.toml');
497
+
498
+ console.log(`
499
+ ${bold(magenta('🧠 Sulcus → OpenAI Codex CLI Configuration Wizard'))}
500
+ ${dim('────────────────────────────────────────────────────────')}
501
+ Configures Sulcus as an MCP server for ${cyan('OpenAI Codex CLI')}.
502
+ Config file: ${cyan('~/.codex/config.toml')}
503
+ Press ${bold('Enter')} to accept defaults. ${bold('Ctrl+C')} to cancel at any time.
504
+ `);
505
+
506
+ // Detect codex CLI
507
+ const codexInstalled = isCliInstalled('codex');
508
+ if (codexInstalled) {
509
+ console.log(` ${green('✓')} codex CLI detected on PATH`);
510
+ } else {
511
+ console.log(` ${yellow('⚠')} codex CLI not found on PATH`);
512
+ console.log(` ${dim('Install from:')} ${cyan('https://github.com/openai/codex')}`);
513
+ }
514
+
515
+ const serverCfg = await askMcpServerConfig();
516
+
517
+ console.log(`${bold('Writing config...')}`);
518
+
519
+ // Read existing TOML and merge in the sulcus entry
520
+ let existing = readTomlConfig(configFile);
521
+ if (!existing.mcp_servers) existing.mcp_servers = {};
522
+ existing.mcp_servers.sulcus = {
523
+ command: serverCfg.command,
524
+ args: serverCfg.args,
525
+ ...(serverCfg.env && Object.keys(serverCfg.env).length > 0
526
+ ? { env: serverCfg.env }
527
+ : {}),
528
+ };
529
+
530
+ try {
531
+ writeTomlConfig(configFile, existing);
532
+ } catch (err) {
533
+ console.log(` ${red('✗')} Failed to write ${cyan(configFile)}: ${err.message}`);
534
+ rl.close();
535
+ process.exit(1);
536
+ }
537
+
538
+ console.log(` ${green('✓')} Written to ${cyan(configFile.replace(os.homedir(), '~'))}`);
539
+ console.log();
540
+
541
+ printMcpSummary('OpenAI Codex CLI', configFile, serverCfg);
542
+
543
+ console.log(` ${bold(green('✅ Configuration complete!'))} Restart Codex to pick up changes.`);
544
+ console.log();
545
+
546
+ rl.close();
547
+ }
548
+
549
+ // ─── Gemini wizard ────────────────────────────────────────────────────────────
550
+
551
+ async function runGeminiWizard() {
552
+ const configFile = expandHome('~/.gemini/settings.json');
553
+
554
+ console.log(`
555
+ ${bold(magenta('🧠 Sulcus → Google Gemini CLI Configuration Wizard'))}
556
+ ${dim('──────────────────────────────────────────────────────────')}
557
+ Configures Sulcus as an MCP server for ${cyan('Google Gemini CLI')}.
558
+ Config file: ${cyan('~/.gemini/settings.json')}
559
+ Press ${bold('Enter')} to accept defaults. ${bold('Ctrl+C')} to cancel at any time.
560
+ `);
561
+
562
+ // Detect gemini CLI
563
+ const geminiInstalled = isCliInstalled('gemini');
564
+ if (geminiInstalled) {
565
+ console.log(` ${green('✓')} gemini CLI detected on PATH`);
566
+ } else {
567
+ console.log(` ${yellow('⚠')} gemini CLI not found on PATH`);
568
+ console.log(` ${dim('Install from:')} ${cyan('https://github.com/google-gemini/gemini-cli')}`);
569
+ }
570
+
571
+ const serverCfg = await askMcpServerConfig();
572
+
573
+ console.log(`${bold('Writing config...')}`);
574
+
575
+ let existing = readJsonConfig(configFile);
576
+ if (!existing.mcpServers) existing.mcpServers = {};
577
+ existing.mcpServers.sulcus = serverCfg;
578
+
579
+ try {
580
+ writeJsonConfig(configFile, existing);
581
+ } catch (err) {
582
+ console.log(` ${red('✗')} Failed to write ${cyan(configFile)}: ${err.message}`);
583
+ rl.close();
584
+ process.exit(1);
585
+ }
586
+
587
+ console.log(` ${green('✓')} Written to ${cyan(configFile.replace(os.homedir(), '~'))}`);
588
+ console.log();
589
+
590
+ printMcpSummary('Google Gemini CLI', configFile, serverCfg);
591
+
592
+ console.log(` ${bold(green('✅ Configuration complete!'))} Restart Gemini CLI to pick up changes.`);
593
+ console.log();
594
+
595
+ rl.close();
596
+ }
597
+
598
+ // ─── openclaw.json discovery ──────────────────────────────────────────────────
599
+
600
+ function findOpenclawJson() {
601
+ const candidates = [
602
+ process.env.OPENCLAW_CONFIG_PATH,
603
+ path.join(os.homedir(), '.openclaw', 'openclaw.json'),
604
+ path.join(process.cwd(), 'openclaw.json'),
605
+ ].filter(Boolean);
606
+
607
+ for (const candidate of candidates) {
608
+ const resolved = expandHome(candidate);
609
+ if (fs.existsSync(resolved)) return resolved;
610
+ }
611
+ return null;
612
+ }
613
+
614
+ // ─── Prebuilt binary download (dylibs for OpenClaw mode) ─────────────────────
615
+
164
616
  function detectPlatform() {
165
617
  const plat = process.platform;
166
618
  const arch = process.arch;
@@ -178,11 +630,6 @@ function detectPlatform() {
178
630
  );
179
631
  }
180
632
 
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
633
  function downloadFile(url, destFile, maxRedirects = 5) {
187
634
  return new Promise((resolve, reject) => {
188
635
  let hops = 0;
@@ -204,13 +651,12 @@ function downloadFile(url, destFile, maxRedirects = 5) {
204
651
  const req = https.request(opts, (res) => {
205
652
  const { statusCode, headers: resHeaders } = res;
206
653
 
207
- // Follow 301/302/307/308 redirects
208
654
  if (
209
655
  (statusCode === 301 || statusCode === 302 ||
210
656
  statusCode === 307 || statusCode === 308) &&
211
657
  resHeaders.location
212
658
  ) {
213
- res.resume(); // drain
659
+ res.resume();
214
660
  return attempt(resHeaders.location);
215
661
  }
216
662
 
@@ -236,7 +682,6 @@ function downloadFile(url, destFile, maxRedirects = 5) {
236
682
  process.stdout.write(`\r Downloading... ${pct}% `);
237
683
  }
238
684
  } else {
239
- // No content-length — show dots
240
685
  if (received % (64 * 1024) === 0) process.stdout.write('.');
241
686
  }
242
687
  });
@@ -262,13 +707,6 @@ function downloadFile(url, destFile, maxRedirects = 5) {
262
707
  });
263
708
  }
264
709
 
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
710
  async function downloadAndInstallBinaries(resolvedLibDir, dylibNames) {
273
711
  let platformInfo;
274
712
  try {
@@ -292,7 +730,6 @@ async function downloadAndInstallBinaries(resolvedLibDir, dylibNames) {
292
730
  return false;
293
731
  }
294
732
 
295
- // Create libDir if needed
296
733
  try {
297
734
  fs.mkdirSync(resolvedLibDir, { recursive: true });
298
735
  } catch (err) {
@@ -301,6 +738,7 @@ async function downloadAndInstallBinaries(resolvedLibDir, dylibNames) {
301
738
  return false;
302
739
  }
303
740
 
741
+
304
742
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sulcus-'));
305
743
  const tarPath = path.join(tmpDir, `sulcus-${platform}.tar.gz`);
306
744
 
@@ -317,7 +755,6 @@ async function downloadAndInstallBinaries(resolvedLibDir, dylibNames) {
317
755
  return false;
318
756
  }
319
757
 
320
- // Extract
321
758
  console.log(` Extracting...`);
322
759
  try {
323
760
  execSync(`tar xzf ${JSON.stringify(tarPath)} -C ${JSON.stringify(tmpDir)}`, { stdio: 'pipe' });
@@ -327,7 +764,6 @@ async function downloadAndInstallBinaries(resolvedLibDir, dylibNames) {
327
764
  return false;
328
765
  }
329
766
 
330
- // Move each dylib into libDir
331
767
  let allInstalled = true;
332
768
  for (const lib of dylibNames) {
333
769
  const srcFile = path.join(tmpDir, lib + ext);
@@ -351,15 +787,14 @@ async function downloadAndInstallBinaries(resolvedLibDir, dylibNames) {
351
787
  }
352
788
  }
353
789
 
354
- // Cleanup temp dir
355
790
  try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {}
356
791
 
357
792
  return allInstalled;
358
793
  }
359
794
 
360
- // ─── Main wizard ──────────────────────────────────────────────────────────────
795
+ // ─── OpenClaw wizard (original, unchanged) ───────────────────────────────────
361
796
 
362
- async function run() {
797
+ async function runOpenclawWizard() {
363
798
  console.log(`
364
799
  ${bold(magenta('🧠 Sulcus Configuration Wizard'))}
365
800
  ${dim('────────────────────────────────────────────')}
@@ -367,7 +802,7 @@ Configures the ${cyan('openclaw-sulcus')} plugin inside your ${cyan('openclaw.js
367
802
  Press ${bold('Enter')} to accept defaults. ${bold('Ctrl+C')} to cancel at any time.
368
803
  `);
369
804
 
370
- // ── Step 1: Locate openclaw.json ──────────────────────────────────────────
805
+ // ── Step 1: Locate openclaw.json ─────────────────────────────────────────
371
806
 
372
807
  console.log(`${bold('Step 1 · Locate openclaw.json')}`);
373
808
 
@@ -379,7 +814,7 @@ Press ${bold('Enter')} to accept defaults. ${bold('Ctrl+C')} to cancel at any ti
379
814
  console.log(` ${yellow('⚠')} Could not find openclaw.json in the usual locations.`);
380
815
  console.log(` Checked:`);
381
816
  if (process.env.OPENCLAW_CONFIG_PATH)
382
- console.log(` • \$OPENCLAW_CONFIG_PATH → ${process.env.OPENCLAW_CONFIG_PATH}`);
817
+ console.log(` • $OPENCLAW_CONFIG_PATH → ${process.env.OPENCLAW_CONFIG_PATH}`);
383
818
  console.log(` • ~/.openclaw/openclaw.json`);
384
819
  console.log(` • ./openclaw.json\n`);
385
820
 
@@ -399,7 +834,6 @@ Press ${bold('Enter')} to accept defaults. ${bold('Ctrl+C')} to cancel at any ti
399
834
  }
400
835
  }
401
836
 
402
- // Read existing config
403
837
  let existingConfig = {};
404
838
  try {
405
839
  existingConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
@@ -410,12 +844,11 @@ Press ${bold('Enter')} to accept defaults. ${bold('Ctrl+C')} to cancel at any ti
410
844
  process.exit(1);
411
845
  }
412
846
 
413
- // ── Step 2: Wizard questions ──────────────────────────────────────────────
847
+ // ── Step 2: Wizard questions ─────────────────────────────────────────────
414
848
 
415
849
  console.log(`${bold('Step 2 · Configure Sulcus')}`);
416
850
  console.log();
417
851
 
418
- // Backend mode
419
852
  console.log(` ${bold('Backend mode:')}`);
420
853
  console.log(` ${cyan('[1]')} Local only ${dim('(WASM + native dylibs, no network)')}`);
421
854
  console.log(` ${cyan('[2]')} Cloud sync ${dim('(local + server replication)')}`);
@@ -423,7 +856,6 @@ Press ${bold('Enter')} to accept defaults. ${bold('Ctrl+C')} to cancel at any ti
423
856
  const cloudSync = modeRaw === '2';
424
857
  console.log();
425
858
 
426
- // Native dylib path
427
859
  const libDirDefault = '~/.sulcus/lib';
428
860
  const libDirRaw = await ask(
429
861
  ` ${bold('Where are your native dylibs?')}`,
@@ -432,11 +864,9 @@ Press ${bold('Enter')} to accept defaults. ${bold('Ctrl+C')} to cancel at any ti
432
864
  const libDir = libDirRaw;
433
865
  console.log();
434
866
 
435
- // Agent namespace
436
867
  const namespace = await ask(` ${bold('Agent namespace:')}`, 'default');
437
868
  console.log();
438
869
 
439
- // Hooks
440
870
  console.log(` ${bold('Enable hooks:')}`);
441
871
  const injectAwareness = await askYN(
442
872
  'Inject memory awareness into prompts? (before_prompt_build)',
@@ -448,7 +878,6 @@ Press ${bold('Enter')} to accept defaults. ${bold('Ctrl+C')} to cancel at any ti
448
878
  );
449
879
  console.log();
450
880
 
451
- // Tools
452
881
  console.log(` ${bold('Enable tools:')}`);
453
882
  const toolMemoryRecall = await askYN('memory_recall — search memories', true);
454
883
  const toolMemoryStore = await askYN('memory_store — save memories', true);
@@ -459,7 +888,6 @@ Press ${bold('Enter')} to accept defaults. ${bold('Ctrl+C')} to cancel at any ti
459
888
  const toolEvalTriggers = await askYN('evaluate_triggers — reactive trigger engine', false);
460
889
  console.log();
461
890
 
462
- // Cloud sync extras
463
891
  let serverUrl = '';
464
892
  let apiKey = '';
465
893
  if (cloudSync) {
@@ -469,7 +897,7 @@ Press ${bold('Enter')} to accept defaults. ${bold('Ctrl+C')} to cancel at any ti
469
897
  console.log();
470
898
  }
471
899
 
472
- // ── Step 3: Build and write config ───────────────────────────────────────
900
+ // ── Step 3: Build and write config ──────────────────────────────────────
473
901
 
474
902
  console.log(`${bold('Step 3 · Write openclaw.json')}`);
475
903
 
@@ -493,12 +921,10 @@ Press ${bold('Enter')} to accept defaults. ${bold('Ctrl+C')} to cancel at any ti
493
921
  },
494
922
  };
495
923
 
496
- // Remove undefined keys
497
924
  Object.keys(sulcusConfig).forEach(
498
925
  (k) => sulcusConfig[k] === undefined && delete sulcusConfig[k],
499
926
  );
500
927
 
501
- // Deep-merge into existing config
502
928
  const merged = deepMerge(existingConfig, {
503
929
  plugins: {
504
930
  entries: {
@@ -524,7 +950,6 @@ Press ${bold('Enter')} to accept defaults. ${bold('Ctrl+C')} to cancel at any ti
524
950
  console.log(` ${green('✓')} Written to ${cyan(configPath)}`);
525
951
  console.log();
526
952
 
527
- // Summary
528
953
  console.log(` ${dim('──── Summary ────────────────────────────────────')}`);
529
954
  console.log(` Plugin: ${cyan('openclaw-sulcus')} ${green('enabled')}`);
530
955
  console.log(` Backend: ${cyan(cloudSync ? 'cloud sync' : 'local only')}`);
@@ -551,7 +976,7 @@ Press ${bold('Enter')} to accept defaults. ${bold('Ctrl+C')} to cancel at any ti
551
976
  console.log();
552
977
  }
553
978
 
554
- // ── Step 4: Validate dylib path (+ auto-download if missing) ─────────────
979
+ // ── Step 4: Validate dylib path (+ auto-download if missing) ────────────
555
980
 
556
981
  console.log(`${bold('Step 4 · Validate')}`);
557
982
 
@@ -561,9 +986,6 @@ Press ${bold('Enter')} to accept defaults. ${bold('Ctrl+C')} to cancel at any ti
561
986
  : process.platform === 'win32' ? '.dll'
562
987
  : '.so';
563
988
 
564
- /**
565
- * Check which dylibs are present. Returns true when all are found.
566
- */
567
989
  function checkDylibs() {
568
990
  if (!fs.existsSync(resolvedLibDir)) return false;
569
991
  return dylibNames.every((lib) => fs.existsSync(path.join(resolvedLibDir, lib + ext)));
@@ -572,24 +994,19 @@ Press ${bold('Enter')} to accept defaults. ${bold('Ctrl+C')} to cancel at any ti
572
994
  let dylibsOk = checkDylibs();
573
995
 
574
996
  if (dylibsOk) {
575
- // All present — just print them
576
997
  for (const lib of dylibNames) {
577
998
  console.log(` ${green('✓')} Found: ${dim(path.join(resolvedLibDir, lib + ext))}`);
578
999
  }
579
1000
  } else {
580
- // Some or all missing — try auto-download
581
1001
  const downloaded = await downloadAndInstallBinaries(resolvedLibDir, dylibNames);
582
1002
 
583
1003
  if (downloaded) {
584
- // Re-validate after successful download
585
1004
  dylibsOk = checkDylibs();
586
1005
  if (!dylibsOk) {
587
1006
  console.log(` ${yellow('⚠')} Some dylibs still missing after installation.`);
588
1007
  }
589
1008
  } else if (!downloaded) {
590
- // Download skipped or failed — show manual instructions
591
1009
  if (fs.existsSync(resolvedLibDir)) {
592
- // Directory exists but files missing — list what we found / didn't find
593
1010
  for (const lib of dylibNames) {
594
1011
  const full = path.join(resolvedLibDir, lib + ext);
595
1012
  if (fs.existsSync(full)) {
@@ -619,6 +1036,19 @@ Press ${bold('Enter')} to accept defaults. ${bold('Ctrl+C')} to cancel at any ti
619
1036
  rl.close();
620
1037
  }
621
1038
 
1039
+ // ─── Entry point ─────────────────────────────────────────────────────────────
1040
+
1041
+ async function run() {
1042
+ const mode = detectMode();
1043
+
1044
+ switch (mode) {
1045
+ case MODE_CLAUDE: return runClaudeWizard();
1046
+ case MODE_OPENAI: return runOpenAIWizard();
1047
+ case MODE_GEMINI: return runGeminiWizard();
1048
+ default: return runOpenclawWizard();
1049
+ }
1050
+ }
1051
+
622
1052
  run().catch((err) => {
623
1053
  console.error(`\n${red('Fatal error:')} ${err.message}\n`);
624
1054
  rl.close();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@digitalforgestudios/openclaw-sulcus",
3
- "version": "3.5.5",
3
+ "version": "3.6.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",