@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.
- package/bin/configure.mjs +502 -72
- 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
|
|
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
|
|
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('
|
|
63
|
-
1. Locates your openclaw.json (checks
|
|
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('
|
|
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
|
-
// ───
|
|
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
|
-
// ───
|
|
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
|
-
*
|
|
162
|
-
* Returns
|
|
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();
|
|
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
|
-
// ───
|
|
795
|
+
// ─── OpenClaw wizard (original, unchanged) ───────────────────────────────────
|
|
361
796
|
|
|
362
|
-
async function
|
|
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(` •
|
|
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.
|
|
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",
|