@digitalforgestudios/openclaw-sulcus 6.6.1 → 6.6.2

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/package.json +1 -5
  2. package/bin/configure.mjs +0 -1056
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@digitalforgestudios/openclaw-sulcus",
3
- "version": "6.6.1",
3
+ "version": "6.6.2",
4
4
  "description": "Sulcus \u2014 thermodynamic memory + Apache AGE knowledge graph for OpenClaw agents. v6.0: Multi-signal recall (semantic + hot-context + entity-graph + profile), configurable guardrails (outputGuard + toolGuard), token budget enforcement, context rebuild post-compaction, sulcus.toml config layer, SIRU training data logging, session-scoped memory, batch heat-boost. SIU v2 pipeline (SIVU/SICU/SILU/SITU/SIRU). Interaction-based decay. Curator sleep-cycle. Cross-agent sync.",
5
5
  "keywords": [
6
6
  "openclaw",
@@ -42,11 +42,7 @@
42
42
  "openclawVersion": "2026.5.2"
43
43
  }
44
44
  },
45
- "bin": {
46
- "openclaw-sulcus": "./bin/configure.mjs"
47
- },
48
45
  "files": [
49
- "bin/",
50
46
  "index.ts",
51
47
  "wasm/",
52
48
  "openclaw.plugin.json",
package/bin/configure.mjs DELETED
@@ -1,1056 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Sulcus Configuration Wizard
4
- * Interactive CLI to configure Sulcus for multiple AI tools.
5
- *
6
- * Usage:
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
11
- * node bin/configure.mjs [--no-color] [--help]
12
- */
13
-
14
- import readline from 'readline';
15
- import fs from 'fs';
16
- import path from 'path';
17
- import os from 'os';
18
- import https from 'https';
19
- import { execSync } from 'child_process';
20
-
21
- // ─── Colour support ───────────────────────────────────────────────────────────
22
-
23
- const noColor =
24
- process.argv.includes('--no-color') ||
25
- process.env.NO_COLOR !== undefined ||
26
- !process.stdout.isTTY;
27
-
28
- const c = {
29
- reset: noColor ? '' : '\x1b[0m',
30
- bold: noColor ? '' : '\x1b[1m',
31
- dim: noColor ? '' : '\x1b[2m',
32
- red: noColor ? '' : '\x1b[31m',
33
- green: noColor ? '' : '\x1b[32m',
34
- yellow: noColor ? '' : '\x1b[33m',
35
- blue: noColor ? '' : '\x1b[34m',
36
- magenta: noColor ? '' : '\x1b[35m',
37
- cyan: noColor ? '' : '\x1b[36m',
38
- white: noColor ? '' : '\x1b[37m',
39
- };
40
-
41
- const bold = (s) => `${c.bold}${s}${c.reset}`;
42
- const dim = (s) => `${c.dim}${s}${c.reset}`;
43
- const green = (s) => `${c.green}${s}${c.reset}`;
44
- const yellow = (s) => `${c.yellow}${s}${c.reset}`;
45
- const red = (s) => `${c.red}${s}${c.reset}`;
46
- const cyan = (s) => `${c.cyan}${s}${c.reset}`;
47
- const magenta = (s) => `${c.magenta}${s}${c.reset}`;
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
-
63
- // ─── Help ─────────────────────────────────────────────────────────────────────
64
-
65
- if (process.argv.includes('--help') || process.argv.includes('-h')) {
66
- console.log(`
67
- ${bold('Sulcus Configuration Wizard')}
68
-
69
- Interactively configure Sulcus as an MCP server for your AI tools.
70
-
71
- ${bold('Usage:')}
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)
80
-
81
- ${bold('Options:')}
82
- --help, -h Show this help message
83
- --no-color Disable coloured output
84
-
85
- ${bold('OpenClaw mode (default):')}
86
- 1. Locates your openclaw.json (checks $OPENCLAW_CONFIG_PATH, ~/.openclaw/, ./)
87
- 2. Walks you through backend mode, dylib path, namespace, hooks, and tools
88
- 3. Deep-merges settings under plugins.entries.openclaw-sulcus.config
89
- 4. Validates that your native dylibs exist and warns if they are missing
90
- 5. Reminds you to restart the OpenClaw gateway
91
-
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:')}
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
103
- `);
104
- process.exit(0);
105
- }
106
-
107
- // ─── Readline helpers ─────────────────────────────────────────────────────────
108
-
109
- const rl = readline.createInterface({
110
- input: process.stdin,
111
- output: process.stdout,
112
- });
113
-
114
- // Graceful Ctrl+C
115
- rl.on('SIGINT', () => {
116
- console.log(`\n\n${yellow('⚡ Wizard cancelled — no changes were written.')}\n`);
117
- process.exit(0);
118
- });
119
-
120
- /**
121
- * Prompt the user with an optional default value.
122
- * Returns the trimmed answer, or the default if empty.
123
- */
124
- function ask(question, defaultValue = '') {
125
- return new Promise((resolve) => {
126
- const hint = defaultValue !== '' ? dim(` [${defaultValue}]`) : '';
127
- rl.question(`${question}${hint} `, (answer) => {
128
- const trimmed = answer.trim();
129
- resolve(trimmed === '' ? defaultValue : trimmed);
130
- });
131
- });
132
- }
133
-
134
- /**
135
- * Ask a yes/no question. Returns boolean.
136
- */
137
- function askYN(question, defaultVal = false) {
138
- return new Promise((resolve) => {
139
- const hint = dim(` [${defaultVal ? 'Y/n' : 'y/N'}]`);
140
- rl.question(` ${question}${hint} `, (answer) => {
141
- const a = answer.trim().toLowerCase();
142
- if (a === '') resolve(defaultVal);
143
- else resolve(a === 'y' || a === 'yes');
144
- });
145
- });
146
- }
147
-
148
- // ─── Utility helpers ──────────────────────────────────────────────────────────
149
-
150
- function expandHome(p) {
151
- if (p.startsWith('~')) return path.join(os.homedir(), p.slice(1));
152
- return p;
153
- }
154
-
155
- // Deep-merge two plain objects (target mutated).
156
- function deepMerge(target, source) {
157
- for (const key of Object.keys(source)) {
158
- if (
159
- source[key] !== null &&
160
- typeof source[key] === 'object' &&
161
- !Array.isArray(source[key]) &&
162
- typeof target[key] === 'object' &&
163
- target[key] !== null &&
164
- !Array.isArray(target[key])
165
- ) {
166
- deepMerge(target[key], source[key]);
167
- } else {
168
- target[key] = source[key];
169
- }
170
- }
171
- return target;
172
- }
173
-
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
- ];
181
-
182
- /**
183
- * Try to locate the sulcus binary.
184
- * Returns the absolute path if found, or null.
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
-
616
- function detectPlatform() {
617
- const plat = process.platform;
618
- const arch = process.arch;
619
-
620
- const ext = plat === 'darwin' ? '.dylib' : '.so';
621
-
622
- if (plat === 'darwin' && arch === 'arm64') return { platform: 'macos-arm64', ext };
623
- if (plat === 'darwin' && arch === 'x64') return { platform: 'macos-x64', ext };
624
- if (plat === 'linux' && arch === 'x64') return { platform: 'linux-x64', ext };
625
- if (plat === 'linux' && arch === 'arm64') return { platform: 'linux-arm64', ext };
626
-
627
- throw new Error(
628
- `Prebuilt binaries are not available for your platform (${plat}/${arch}).\n` +
629
- ` Supported: darwin/arm64, darwin/x64, linux/x64, linux/arm64`,
630
- );
631
- }
632
-
633
- function downloadFile(url, destFile, maxRedirects = 5) {
634
- return new Promise((resolve, reject) => {
635
- let hops = 0;
636
-
637
- function attempt(currentUrl) {
638
- if (hops > maxRedirects) {
639
- return reject(new Error('Too many redirects while downloading'));
640
- }
641
- hops++;
642
-
643
- const parsed = new URL(currentUrl);
644
- const opts = {
645
- hostname: parsed.hostname,
646
- path: parsed.pathname + parsed.search,
647
- method: 'GET',
648
- headers: { 'User-Agent': 'sulcus-configure/1.0' },
649
- };
650
-
651
- const req = https.request(opts, (res) => {
652
- const { statusCode, headers: resHeaders } = res;
653
-
654
- if (
655
- (statusCode === 301 || statusCode === 302 ||
656
- statusCode === 307 || statusCode === 308) &&
657
- resHeaders.location
658
- ) {
659
- res.resume();
660
- return attempt(resHeaders.location);
661
- }
662
-
663
- if (statusCode !== 200) {
664
- res.resume();
665
- return reject(new Error(`HTTP ${statusCode} for ${currentUrl}`));
666
- }
667
-
668
- const total = parseInt(resHeaders['content-length'] || '0', 10);
669
- let received = 0;
670
- let lastPct = -1;
671
-
672
- const out = fs.createWriteStream(destFile);
673
-
674
- res.on('data', (chunk) => {
675
- received += chunk.length;
676
- out.write(chunk);
677
-
678
- if (total > 0) {
679
- const pct = Math.floor((received / total) * 100);
680
- if (pct !== lastPct && pct % 5 === 0) {
681
- lastPct = pct;
682
- process.stdout.write(`\r Downloading... ${pct}% `);
683
- }
684
- } else {
685
- if (received % (64 * 1024) === 0) process.stdout.write('.');
686
- }
687
- });
688
-
689
- res.on('end', () => {
690
- out.end(() => {
691
- process.stdout.write(`\r Downloaded ${(received / 1024 / 1024).toFixed(1)} MB \n`);
692
- resolve();
693
- });
694
- });
695
-
696
- res.on('error', (err) => {
697
- out.destroy();
698
- reject(err);
699
- });
700
- });
701
-
702
- req.on('error', reject);
703
- req.end();
704
- }
705
-
706
- attempt(url);
707
- });
708
- }
709
-
710
- async function downloadAndInstallBinaries(resolvedLibDir, dylibNames) {
711
- let platformInfo;
712
- try {
713
- platformInfo = detectPlatform();
714
- } catch (err) {
715
- console.log(` ${yellow('⚠')} ${err.message}`);
716
- return false;
717
- }
718
-
719
- const { platform, ext } = platformInfo;
720
- const displayDir = resolvedLibDir.replace(os.homedir(), '~');
721
- const tarUrl = `https://github.com/digitalforgeca/sulcus/releases/latest/download/sulcus-${platform}.tar.gz`;
722
-
723
- console.log();
724
- console.log(` ${yellow('⚠')} Native libraries not found at ${cyan(displayDir)}`);
725
- console.log(` ${dim(`Download prebuilt binaries for ${bold(platform)}?`)}`);
726
-
727
- const doDownload = await askYN(`Download prebuilt binaries for ${platform}?`, true);
728
- if (!doDownload) {
729
- console.log(` ${dim('Skipped. Install dylibs manually to use Sulcus.')}`);
730
- return false;
731
- }
732
-
733
- try {
734
- fs.mkdirSync(resolvedLibDir, { recursive: true });
735
- } catch (err) {
736
- console.log(` ${red('✗')} Cannot create ${cyan(resolvedLibDir)}: ${err.message}`);
737
- console.log(` ${dim('Try running with appropriate permissions.')}`);
738
- return false;
739
- }
740
-
741
-
742
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sulcus-'));
743
- const tarPath = path.join(tmpDir, `sulcus-${platform}.tar.gz`);
744
-
745
- console.log(` ${dim(`→ ${tarUrl}`)}`);
746
- process.stdout.write(` Downloading...`);
747
-
748
- try {
749
- await downloadFile(tarUrl, tarPath);
750
- } catch (err) {
751
- console.log(` ${red('✗')} Download failed: ${err.message}`);
752
- console.log(` ${dim('Check your internet connection or download manually:')}`);
753
- console.log(` ${cyan(tarUrl)}`);
754
- try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {}
755
- return false;
756
- }
757
-
758
- console.log(` Extracting...`);
759
- try {
760
- execSync(`tar xzf ${JSON.stringify(tarPath)} -C ${JSON.stringify(tmpDir)}`, { stdio: 'pipe' });
761
- } catch (err) {
762
- console.log(` ${red('✗')} Extraction failed: ${err.message}`);
763
- try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {}
764
- return false;
765
- }
766
-
767
- let allInstalled = true;
768
- for (const lib of dylibNames) {
769
- const srcFile = path.join(tmpDir, lib + ext);
770
- const destFile = path.join(resolvedLibDir, lib + ext);
771
-
772
- if (!fs.existsSync(srcFile)) {
773
- console.log(` ${yellow('⚠')} ${lib + ext} not found in tarball`);
774
- allInstalled = false;
775
- continue;
776
- }
777
-
778
- try {
779
- fs.copyFileSync(srcFile, destFile);
780
- console.log(` ${green('✓')} Installed: ${dim(destFile)}`);
781
- } catch (err) {
782
- console.log(` ${red('✗')} Failed to install ${lib + ext}: ${err.message}`);
783
- if (err.code === 'EACCES') {
784
- console.log(` ${dim('Try running with appropriate permissions (e.g. sudo).')}`);
785
- }
786
- allInstalled = false;
787
- }
788
- }
789
-
790
- try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {}
791
-
792
- return allInstalled;
793
- }
794
-
795
- // ─── OpenClaw wizard (original, unchanged) ───────────────────────────────────
796
-
797
- async function runOpenclawWizard() {
798
- console.log(`
799
- ${bold(magenta('🧠 Sulcus Configuration Wizard'))}
800
- ${dim('────────────────────────────────────────────')}
801
- Configures the ${cyan('openclaw-sulcus')} plugin inside your ${cyan('openclaw.json')}.
802
- Press ${bold('Enter')} to accept defaults. ${bold('Ctrl+C')} to cancel at any time.
803
- `);
804
-
805
- // ── Step 1: Locate openclaw.json ─────────────────────────────────────────
806
-
807
- console.log(`${bold('Step 1 · Locate openclaw.json')}`);
808
-
809
- let configPath = findOpenclawJson();
810
-
811
- if (configPath) {
812
- console.log(` ${green('✓')} Found: ${cyan(configPath)}\n`);
813
- } else {
814
- console.log(` ${yellow('⚠')} Could not find openclaw.json in the usual locations.`);
815
- console.log(` Checked:`);
816
- if (process.env.OPENCLAW_CONFIG_PATH)
817
- console.log(` • $OPENCLAW_CONFIG_PATH → ${process.env.OPENCLAW_CONFIG_PATH}`);
818
- console.log(` • ~/.openclaw/openclaw.json`);
819
- console.log(` • ./openclaw.json\n`);
820
-
821
- const choice = await ask(
822
- ` Enter full path to openclaw.json, or press Enter to create ~/.openclaw/openclaw.json:`,
823
- path.join(os.homedir(), '.openclaw', 'openclaw.json'),
824
- );
825
- configPath = expandHome(choice);
826
-
827
- if (!fs.existsSync(configPath)) {
828
- const dir = path.dirname(configPath);
829
- fs.mkdirSync(dir, { recursive: true });
830
- fs.writeFileSync(configPath, JSON.stringify({}, null, 2), 'utf8');
831
- console.log(` ${green('✓')} Created: ${cyan(configPath)}\n`);
832
- } else {
833
- console.log(` ${green('✓')} Using: ${cyan(configPath)}\n`);
834
- }
835
- }
836
-
837
- let existingConfig = {};
838
- try {
839
- existingConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
840
- } catch (err) {
841
- console.log(` ${red('✗')} Failed to parse openclaw.json: ${err.message}`);
842
- console.log(` Fix the JSON syntax and re-run the wizard.\n`);
843
- rl.close();
844
- process.exit(1);
845
- }
846
-
847
- // ── Step 2: Wizard questions ─────────────────────────────────────────────
848
-
849
- console.log(`${bold('Step 2 · Configure Sulcus')}`);
850
- console.log();
851
-
852
- console.log(` ${bold('Backend mode:')}`);
853
- console.log(` ${cyan('[1]')} Local only ${dim('(WASM + native dylibs, no network)')}`);
854
- console.log(` ${cyan('[2]')} Cloud sync ${dim('(local + server replication)')}`);
855
- const modeRaw = await ask(` >`, '1');
856
- const cloudSync = modeRaw === '2';
857
- console.log();
858
-
859
- const libDirDefault = '~/.sulcus/lib';
860
- const libDirRaw = await ask(
861
- ` ${bold('Where are your native dylibs?')}`,
862
- libDirDefault,
863
- );
864
- const libDir = libDirRaw;
865
- console.log();
866
-
867
- const namespace = await ask(` ${bold('Agent namespace:')}`, 'default');
868
- console.log();
869
-
870
- console.log(` ${bold('Enable hooks:')}`);
871
- const injectAwareness = await askYN(
872
- 'Inject memory awareness into prompts? (before_prompt_build)',
873
- false,
874
- );
875
- const autoRecall = await askYN(
876
- 'Auto-recall memories on each turn? (before_agent_start)',
877
- false,
878
- );
879
- console.log();
880
-
881
- console.log(` ${bold('Enable tools:')}`);
882
- const toolMemoryRecall = await askYN('memory_recall — search memories', true);
883
- const toolMemoryStore = await askYN('memory_store — save memories', true);
884
- const toolMemoryStatus = await askYN('memory_status — check memory stats', true);
885
- const toolConsolidate = await askYN('consolidate — cluster similar memories', false);
886
- const toolExportMarkdown = await askYN('export_markdown — export memories as markdown', false);
887
- const toolImportMarkdown = await askYN('import_markdown — import from markdown', false);
888
- const toolEvalTriggers = await askYN('evaluate_triggers — reactive trigger engine', false);
889
- console.log();
890
-
891
- let serverUrl = '';
892
- let apiKey = '';
893
- if (cloudSync) {
894
- console.log(` ${bold('Cloud sync settings:')}`);
895
- serverUrl = await ask(` Server URL:`, 'https://api.sulcus.ca');
896
- apiKey = await ask(` API Key:`, '');
897
- console.log();
898
- }
899
-
900
- // ── Step 3: Build and write config ──────────────────────────────────────
901
-
902
- console.log(`${bold('Step 3 · Write openclaw.json')}`);
903
-
904
- const sulcusConfig = {
905
- libDir,
906
- namespace: namespace === 'default' ? undefined : namespace,
907
- ...(cloudSync && serverUrl ? { serverUrl } : {}),
908
- ...(cloudSync && apiKey ? { apiKey } : {}),
909
- hooks: {
910
- before_prompt_build: { action: 'inject_awareness', enabled: injectAwareness },
911
- before_agent_start: { action: 'auto_recall', enabled: autoRecall, limit: 5, minScore: 0.3 },
912
- },
913
- tools: {
914
- memory_recall: { enabled: toolMemoryRecall },
915
- memory_store: { enabled: toolMemoryStore },
916
- memory_status: { enabled: toolMemoryStatus },
917
- consolidate: { enabled: toolConsolidate },
918
- export_markdown: { enabled: toolExportMarkdown },
919
- import_markdown: { enabled: toolImportMarkdown },
920
- evaluate_triggers: { enabled: toolEvalTriggers },
921
- },
922
- };
923
-
924
- Object.keys(sulcusConfig).forEach(
925
- (k) => sulcusConfig[k] === undefined && delete sulcusConfig[k],
926
- );
927
-
928
- const merged = deepMerge(existingConfig, {
929
- plugins: {
930
- entries: {
931
- 'openclaw-sulcus': {
932
- enabled: true,
933
- config: sulcusConfig,
934
- },
935
- },
936
- },
937
- });
938
-
939
- let written = false;
940
- try {
941
- fs.writeFileSync(configPath, JSON.stringify(merged, null, 2) + '\n', 'utf8');
942
- written = true;
943
- } catch (err) {
944
- console.log(` ${red('✗')} Failed to write ${configPath}: ${err.message}\n`);
945
- rl.close();
946
- process.exit(1);
947
- }
948
-
949
- if (written) {
950
- console.log(` ${green('✓')} Written to ${cyan(configPath)}`);
951
- console.log();
952
-
953
- console.log(` ${dim('──── Summary ────────────────────────────────────')}`);
954
- console.log(` Plugin: ${cyan('openclaw-sulcus')} ${green('enabled')}`);
955
- console.log(` Backend: ${cyan(cloudSync ? 'cloud sync' : 'local only')}`);
956
- console.log(` Dylib dir: ${cyan(libDir)}`);
957
- console.log(` Namespace: ${cyan(namespace)}`);
958
- if (cloudSync && serverUrl) console.log(` Server: ${cyan(serverUrl)}`);
959
-
960
- const enabledHooks = [];
961
- if (injectAwareness) enabledHooks.push('before_prompt_build');
962
- if (autoRecall) enabledHooks.push('before_agent_start');
963
- console.log(` Hooks: ${enabledHooks.length ? cyan(enabledHooks.join(', ')) : dim('(none enabled)')}`);
964
-
965
- const enabledTools = [
966
- toolMemoryRecall && 'memory_recall',
967
- toolMemoryStore && 'memory_store',
968
- toolMemoryStatus && 'memory_status',
969
- toolConsolidate && 'consolidate',
970
- toolExportMarkdown && 'export_markdown',
971
- toolImportMarkdown && 'import_markdown',
972
- toolEvalTriggers && 'evaluate_triggers',
973
- ].filter(Boolean);
974
- console.log(` Tools: ${enabledTools.length ? cyan(enabledTools.join(', ')) : dim('(none enabled)')}`);
975
- console.log(` ${dim('─────────────────────────────────────────────────')}`);
976
- console.log();
977
- }
978
-
979
- // ── Step 4: Validate dylib path (+ auto-download if missing) ────────────
980
-
981
- console.log(`${bold('Step 4 · Validate')}`);
982
-
983
- const resolvedLibDir = expandHome(libDir);
984
- const dylibNames = ['libsulcus_store', 'libsulcus_vectors'];
985
- const ext = process.platform === 'darwin' ? '.dylib'
986
- : process.platform === 'win32' ? '.dll'
987
- : '.so';
988
-
989
- function checkDylibs() {
990
- if (!fs.existsSync(resolvedLibDir)) return false;
991
- return dylibNames.every((lib) => fs.existsSync(path.join(resolvedLibDir, lib + ext)));
992
- }
993
-
994
- let dylibsOk = checkDylibs();
995
-
996
- if (dylibsOk) {
997
- for (const lib of dylibNames) {
998
- console.log(` ${green('✓')} Found: ${dim(path.join(resolvedLibDir, lib + ext))}`);
999
- }
1000
- } else {
1001
- const downloaded = await downloadAndInstallBinaries(resolvedLibDir, dylibNames);
1002
-
1003
- if (downloaded) {
1004
- dylibsOk = checkDylibs();
1005
- if (!dylibsOk) {
1006
- console.log(` ${yellow('⚠')} Some dylibs still missing after installation.`);
1007
- }
1008
- } else if (!downloaded) {
1009
- if (fs.existsSync(resolvedLibDir)) {
1010
- for (const lib of dylibNames) {
1011
- const full = path.join(resolvedLibDir, lib + ext);
1012
- if (fs.existsSync(full)) {
1013
- console.log(` ${green('✓')} Found: ${dim(full)}`);
1014
- } else {
1015
- console.log(` ${yellow('⚠')} Missing: ${dim(full)}`);
1016
- }
1017
- }
1018
- }
1019
- console.log();
1020
- console.log(` ${yellow(bold('Native dylibs missing — Sulcus will not load.'))}`);
1021
- console.log(` Download manually from:`);
1022
- console.log(` ${cyan('https://github.com/digitalforgeca/sulcus/releases/latest')}`);
1023
- console.log(` Or visit: ${cyan('https://sulcus.ca/docs/install')}`);
1024
- }
1025
- }
1026
-
1027
- if (dylibsOk) {
1028
- console.log(` ${green('✓')} All dylibs present — Sulcus is ready to go.`);
1029
- }
1030
-
1031
- console.log();
1032
- console.log(` ${bold(green('✅ Configuration complete!'))} Restart the OpenClaw gateway to pick up changes:`);
1033
- console.log(` ${cyan('openclaw gateway restart')}`);
1034
- console.log();
1035
-
1036
- rl.close();
1037
- }
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
-
1052
- run().catch((err) => {
1053
- console.error(`\n${red('Fatal error:')} ${err.message}\n`);
1054
- rl.close();
1055
- process.exit(1);
1056
- });