wolverine-ai 6.1.2 → 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,871 @@
1
+ /**
2
+ * Wolverine Claw Setup — Onboarding for OpenClaw users.
3
+ *
4
+ * Detects an existing OpenClaw installation, reads its config, merges with
5
+ * wolverine defaults, scaffolds wolverine-claw/, validates the environment,
6
+ * and produces a ready-to-run dev setup.
7
+ *
8
+ * Run via:
9
+ * wolverine-claw --setup
10
+ * wolverine --setup-claw
11
+ * npx wolverine-ai --setup-claw
12
+ */
13
+
14
+ const fs = require("fs");
15
+ const path = require("path");
16
+ const { execSync } = require("child_process");
17
+ const chalk = require("chalk");
18
+ const os = require("os");
19
+
20
+ // ── Detection ────────────────────────────────────────────────────
21
+
22
+ /**
23
+ * Detect environment: Node version, OpenClaw installation, API keys, OS.
24
+ */
25
+ function detectEnvironment(cwd) {
26
+ const env = {
27
+ node: {
28
+ version: process.version,
29
+ major: parseInt(process.version.slice(1), 10),
30
+ ok: parseInt(process.version.slice(1), 10) >= 22,
31
+ },
32
+ os: {
33
+ platform: process.platform,
34
+ arch: process.arch,
35
+ hostname: os.hostname(),
36
+ },
37
+ openclaw: detectOpenClaw(cwd),
38
+ keys: detectApiKeys(),
39
+ wolverine: detectWolverine(cwd),
40
+ existingClaw: fs.existsSync(path.join(cwd, "wolverine-claw", "index.js")),
41
+ };
42
+
43
+ return env;
44
+ }
45
+
46
+ /**
47
+ * Find OpenClaw installation — checks npm, global, config file.
48
+ */
49
+ function detectOpenClaw(cwd) {
50
+ const result = {
51
+ found: false,
52
+ source: null,
53
+ version: null,
54
+ configPath: null,
55
+ config: null,
56
+ globalInstall: false,
57
+ localInstall: false,
58
+ };
59
+
60
+ // 1. Check local node_modules
61
+ try {
62
+ const pkgPath = require.resolve("openclaw/package.json", { paths: [cwd] });
63
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
64
+ result.found = true;
65
+ result.localInstall = true;
66
+ result.version = pkg.version;
67
+ result.source = "npm (local)";
68
+ } catch {}
69
+
70
+ // 2. Check global install
71
+ if (!result.found) {
72
+ try {
73
+ const ver = execSync("npx --yes openclaw --version 2>/dev/null", {
74
+ encoding: "utf-8",
75
+ timeout: 15000,
76
+ stdio: ["pipe", "pipe", "pipe"],
77
+ }).trim();
78
+ if (ver && /\d+\.\d+/.test(ver)) {
79
+ result.found = true;
80
+ result.globalInstall = true;
81
+ result.version = ver;
82
+ result.source = "npm (global/npx)";
83
+ }
84
+ } catch {}
85
+ }
86
+
87
+ // 3. Check package.json for openclaw dependency
88
+ if (!result.found) {
89
+ try {
90
+ const pkg = JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf-8"));
91
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies, ...pkg.optionalDependencies };
92
+ if (deps.openclaw) {
93
+ result.found = true;
94
+ result.source = "package.json (not installed yet)";
95
+ result.version = deps.openclaw;
96
+ }
97
+ } catch {}
98
+ }
99
+
100
+ // 4. Look for OpenClaw config file
101
+ const configLocations = [
102
+ path.join(os.homedir(), ".openclaw", "config.yml"),
103
+ path.join(os.homedir(), ".openclaw", "config.yaml"),
104
+ path.join(cwd, ".openclaw", "config.yml"),
105
+ path.join(cwd, "openclaw.yml"),
106
+ path.join(cwd, "openclaw.yaml"),
107
+ path.join(cwd, ".openclaw.yml"),
108
+ ];
109
+
110
+ for (const loc of configLocations) {
111
+ if (fs.existsSync(loc)) {
112
+ result.configPath = loc;
113
+ result.config = parseOpenClawConfig(loc);
114
+ if (!result.found) {
115
+ result.found = true;
116
+ result.source = `config file (${path.basename(loc)})`;
117
+ }
118
+ break;
119
+ }
120
+ }
121
+
122
+ return result;
123
+ }
124
+
125
+ /**
126
+ * Parse OpenClaw config YAML (simple line-based parser, no dep needed).
127
+ */
128
+ function parseOpenClawConfig(filePath) {
129
+ try {
130
+ const raw = fs.readFileSync(filePath, "utf-8");
131
+ const config = {};
132
+ let currentSection = null;
133
+ let currentSubsection = null;
134
+
135
+ for (const line of raw.split("\n")) {
136
+ const trimmed = line.trimEnd();
137
+ if (!trimmed || trimmed.startsWith("#")) continue;
138
+
139
+ // Top-level key (no indent)
140
+ const topMatch = trimmed.match(/^(\w[\w-]*):\s*(.*)?$/);
141
+ if (topMatch && !line.startsWith(" ") && !line.startsWith("\t")) {
142
+ currentSection = topMatch[1];
143
+ currentSubsection = null;
144
+ const val = topMatch[2]?.trim();
145
+ if (val && val !== "") {
146
+ config[currentSection] = parseYamlValue(val);
147
+ } else {
148
+ config[currentSection] = {};
149
+ }
150
+ continue;
151
+ }
152
+
153
+ // Second-level key (2-space indent)
154
+ const subMatch = trimmed.match(/^\s{2}(\w[\w-]*):\s*(.*)?$/);
155
+ if (subMatch && currentSection) {
156
+ if (typeof config[currentSection] !== "object") config[currentSection] = {};
157
+ const key = subMatch[1];
158
+ const val = subMatch[2]?.trim();
159
+ if (val && val !== "") {
160
+ config[currentSection][key] = parseYamlValue(val);
161
+ } else {
162
+ config[currentSection][key] = {};
163
+ currentSubsection = key;
164
+ }
165
+ continue;
166
+ }
167
+
168
+ // Third-level key (4-space indent)
169
+ const deepMatch = trimmed.match(/^\s{4}(\w[\w-]*):\s*(.*)?$/);
170
+ if (deepMatch && currentSection && currentSubsection) {
171
+ if (typeof config[currentSection][currentSubsection] !== "object") {
172
+ config[currentSection][currentSubsection] = {};
173
+ }
174
+ config[currentSection][currentSubsection][deepMatch[1]] = parseYamlValue(deepMatch[2]?.trim() || "");
175
+ }
176
+ }
177
+
178
+ return config;
179
+ } catch {
180
+ return null;
181
+ }
182
+ }
183
+
184
+ function parseYamlValue(val) {
185
+ if (!val || val === "") return "";
186
+ if (val === "true") return true;
187
+ if (val === "false") return false;
188
+ if (/^\d+$/.test(val)) return parseInt(val, 10);
189
+ if (/^\d+\.\d+$/.test(val)) return parseFloat(val);
190
+ // Strip quotes
191
+ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
192
+ return val.slice(1, -1);
193
+ }
194
+ // Array
195
+ if (val.startsWith("[") && val.endsWith("]")) {
196
+ try { return JSON.parse(val); } catch {}
197
+ return val.slice(1, -1).split(",").map(s => s.trim().replace(/^["']|["']$/g, ""));
198
+ }
199
+ return val;
200
+ }
201
+
202
+ /**
203
+ * Detect API keys from environment and .env files.
204
+ */
205
+ function detectApiKeys() {
206
+ const keys = {
207
+ OPENAI_API_KEY: { set: false, source: null },
208
+ ANTHROPIC_API_KEY: { set: false, source: null },
209
+ WOLVERINE_API_KEY: { set: false, source: null },
210
+ WOLVERINE_ADMIN_KEY: { set: false, source: null },
211
+ };
212
+
213
+ // Check process.env (already loaded from dotenv)
214
+ for (const key of Object.keys(keys)) {
215
+ if (process.env[key]) {
216
+ keys[key].set = true;
217
+ keys[key].source = "environment";
218
+ }
219
+ }
220
+
221
+ // Check .env.local if not already loaded
222
+ const envLocalPath = path.resolve(process.cwd(), ".env.local");
223
+ if (fs.existsSync(envLocalPath)) {
224
+ try {
225
+ const envContent = fs.readFileSync(envLocalPath, "utf-8");
226
+ for (const key of Object.keys(keys)) {
227
+ const match = envContent.match(new RegExp(`^${key}=(.+)$`, "m"));
228
+ if (match && match[1].trim()) {
229
+ keys[key].set = true;
230
+ keys[key].source = keys[key].source || ".env.local";
231
+ }
232
+ }
233
+ } catch {}
234
+ }
235
+
236
+ return keys;
237
+ }
238
+
239
+ /**
240
+ * Detect existing wolverine installation.
241
+ */
242
+ function detectWolverine(cwd) {
243
+ const result = {
244
+ installed: false,
245
+ version: null,
246
+ serverExists: fs.existsSync(path.join(cwd, "server", "index.js")),
247
+ brainExists: fs.existsSync(path.join(cwd, ".wolverine", "brain")),
248
+ };
249
+
250
+ try {
251
+ const pkg = JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf-8"));
252
+ if (pkg.name === "wolverine-ai" || pkg.dependencies?.["wolverine-ai"]) {
253
+ result.installed = true;
254
+ result.version = pkg.version || pkg.dependencies?.["wolverine-ai"];
255
+ }
256
+ // Also check if we're inside the wolverine repo itself
257
+ if (fs.existsSync(path.join(cwd, "src", "core", "wolverine.js"))) {
258
+ result.installed = true;
259
+ result.version = pkg.version;
260
+ }
261
+ } catch {}
262
+
263
+ return result;
264
+ }
265
+
266
+ // ── Config Merge ────────────────────────────────────────────────
267
+
268
+ /**
269
+ * Merge OpenClaw config with wolverine-claw defaults.
270
+ * OpenClaw values take precedence where they exist.
271
+ */
272
+ function mergeConfig(openclawConfig, defaults) {
273
+ const merged = JSON.parse(JSON.stringify(defaults));
274
+
275
+ if (!openclawConfig) return merged;
276
+
277
+ // Gateway
278
+ if (openclawConfig.gateway) {
279
+ if (openclawConfig.gateway.port) merged.gateway.port = openclawConfig.gateway.port;
280
+ if (openclawConfig.gateway.host) merged.gateway.host = openclawConfig.gateway.host;
281
+ }
282
+
283
+ // Agent / model
284
+ if (openclawConfig.agent) {
285
+ if (openclawConfig.agent.model) merged.agent.model = openclawConfig.agent.model;
286
+ if (openclawConfig.agent.maxTurns) merged.agent.maxTurns = openclawConfig.agent.maxTurns;
287
+ }
288
+
289
+ // Models — if openclaw specifies a model, use it across all roles
290
+ if (openclawConfig.model) {
291
+ merged.agent.model = openclawConfig.model;
292
+ merged.models.reasoning = openclawConfig.model;
293
+ merged.models.coding = openclawConfig.model;
294
+ merged.models.chat = openclawConfig.model;
295
+ }
296
+
297
+ // Channels — merge any configured channels
298
+ if (openclawConfig.channels && typeof openclawConfig.channels === "object") {
299
+ for (const [name, cfg] of Object.entries(openclawConfig.channels)) {
300
+ if (typeof cfg !== "object") continue;
301
+ if (merged.channels[name]) {
302
+ merged.channels[name] = { ...merged.channels[name], ...cfg, enabled: true };
303
+ } else {
304
+ merged.channels[name] = { enabled: true, ...cfg };
305
+ }
306
+ }
307
+ }
308
+
309
+ // Skills
310
+ if (openclawConfig.skills && typeof openclawConfig.skills === "object") {
311
+ for (const [name, cfg] of Object.entries(openclawConfig.skills)) {
312
+ if (typeof cfg === "object") {
313
+ merged.skills[name] = { ...merged.skills[name], ...cfg };
314
+ }
315
+ }
316
+ }
317
+
318
+ // Security
319
+ if (openclawConfig.security && typeof openclawConfig.security === "object") {
320
+ merged.security = { ...merged.security, ...openclawConfig.security };
321
+ }
322
+
323
+ return merged;
324
+ }
325
+
326
+ // ── Scaffolding ─────────────────────────────────────────────────
327
+
328
+ /**
329
+ * Scaffold the wolverine-claw directory from template + merged config.
330
+ */
331
+ function scaffold(cwd, mergedConfig, env) {
332
+ const clawDir = path.join(cwd, "wolverine-claw");
333
+ const results = { created: [], skipped: [], errors: [] };
334
+
335
+ // Create directories
336
+ const dirs = ["config", "plugins", "workspace", "skills"];
337
+ for (const d of dirs) {
338
+ const dirPath = path.join(clawDir, d);
339
+ if (!fs.existsSync(dirPath)) {
340
+ fs.mkdirSync(dirPath, { recursive: true });
341
+ results.created.push(`wolverine-claw/${d}/`);
342
+ }
343
+ }
344
+
345
+ // Write config/settings.json (never overwrite if exists)
346
+ const configPath = path.join(clawDir, "config", "settings.json");
347
+ if (fs.existsSync(configPath)) {
348
+ results.skipped.push("config/settings.json (already exists)");
349
+ } else {
350
+ fs.writeFileSync(configPath, JSON.stringify(mergedConfig, null, 2) + "\n");
351
+ results.created.push("config/settings.json");
352
+ }
353
+
354
+ // Copy index.js from template
355
+ const indexSrc = path.join(cwd, "wolverine-claw", "index.js");
356
+ if (!fs.existsSync(indexSrc)) {
357
+ // Copy from our built-in template
358
+ const templateIndex = path.join(__dirname, "..", "..", "wolverine-claw", "index.js");
359
+ if (fs.existsSync(templateIndex)) {
360
+ fs.copyFileSync(templateIndex, indexSrc);
361
+ results.created.push("index.js");
362
+ }
363
+ } else {
364
+ results.skipped.push("index.js (already exists)");
365
+ }
366
+
367
+ // Copy plugin
368
+ const pluginDest = path.join(clawDir, "plugins", "wolverine-integration.js");
369
+ if (!fs.existsSync(pluginDest)) {
370
+ const pluginSrc = path.join(cwd, "wolverine-claw", "plugins", "wolverine-integration.js");
371
+ if (fs.existsSync(pluginSrc)) {
372
+ results.skipped.push("plugins/wolverine-integration.js (already exists at source)");
373
+ } else {
374
+ const templatePlugin = path.join(__dirname, "..", "..", "wolverine-claw", "plugins", "wolverine-integration.js");
375
+ if (fs.existsSync(templatePlugin)) {
376
+ fs.copyFileSync(templatePlugin, pluginDest);
377
+ results.created.push("plugins/wolverine-integration.js");
378
+ }
379
+ }
380
+ } else {
381
+ results.skipped.push("plugins/wolverine-integration.js (already exists)");
382
+ }
383
+
384
+ // Create workspace/.gitkeep
385
+ const gitkeep = path.join(clawDir, "workspace", ".gitkeep");
386
+ if (!fs.existsSync(gitkeep)) {
387
+ fs.writeFileSync(gitkeep, "");
388
+ results.created.push("workspace/.gitkeep");
389
+ }
390
+
391
+ // Create .gitignore for workspace (agent-generated files shouldn't pollute git)
392
+ const wsGitignore = path.join(clawDir, "workspace", ".gitignore");
393
+ if (!fs.existsSync(wsGitignore)) {
394
+ fs.writeFileSync(wsGitignore, [
395
+ "# Wolverine Claw workspace — agent-generated files",
396
+ "# Keep .gitkeep but ignore everything else",
397
+ "*",
398
+ "!.gitkeep",
399
+ "!.gitignore",
400
+ "",
401
+ ].join("\n"));
402
+ results.created.push("workspace/.gitignore");
403
+ }
404
+
405
+ // Create skills/.gitkeep for custom user skills
406
+ const skillsGitkeep = path.join(clawDir, "skills", ".gitkeep");
407
+ if (!fs.existsSync(skillsGitkeep)) {
408
+ fs.writeFileSync(skillsGitkeep, "");
409
+ results.created.push("skills/.gitkeep");
410
+ }
411
+
412
+ return results;
413
+ }
414
+
415
+ // ── Environment Setup ───────────────────────────────────────────
416
+
417
+ /**
418
+ * Ensure .env.local exists with required keys for claw.
419
+ */
420
+ function ensureEnvFile(cwd, env) {
421
+ const envPath = path.join(cwd, ".env.local");
422
+ const result = { created: false, keysAdded: [] };
423
+
424
+ if (fs.existsSync(envPath)) {
425
+ // Check if claw-specific keys need to be appended
426
+ const content = fs.readFileSync(envPath, "utf-8");
427
+ const clawKeys = [];
428
+
429
+ if (!content.includes("OPENCLAW_")) {
430
+ clawKeys.push("");
431
+ clawKeys.push("# ── Wolverine Claw (OpenClaw Integration) ───────────────────────");
432
+ clawKeys.push("# Channel tokens — add these if you enable messaging channels");
433
+ clawKeys.push("# DISCORD_BOT_TOKEN=");
434
+ clawKeys.push("# SLACK_BOT_TOKEN=");
435
+ clawKeys.push("# SLACK_APP_TOKEN=");
436
+ clawKeys.push("# TELEGRAM_BOT_TOKEN=");
437
+ }
438
+
439
+ if (clawKeys.length > 0) {
440
+ fs.appendFileSync(envPath, clawKeys.join("\n") + "\n");
441
+ result.keysAdded = clawKeys.filter(k => k.startsWith("# ") && k.includes("="));
442
+ }
443
+ } else {
444
+ // Create fresh .env.local
445
+ const lines = [
446
+ "# Wolverine + Claw — Secrets Only",
447
+ "# All other settings in wolverine-claw/config/settings.json",
448
+ "",
449
+ "# ── AI API Keys (at least one required) ──────────────────────────",
450
+ "OPENAI_API_KEY=",
451
+ "ANTHROPIC_API_KEY=",
452
+ "",
453
+ "# ── Dashboard Admin Key ──────────────────────────────────────────",
454
+ "WOLVERINE_ADMIN_KEY=",
455
+ "",
456
+ "# ── Wolverine Platform (optional) ────────────────────────────────",
457
+ "WOLVERINE_API_KEY=",
458
+ "",
459
+ "# ── Wolverine Claw (OpenClaw Integration) ───────────────────────",
460
+ "# Channel tokens — add these if you enable messaging channels",
461
+ "# DISCORD_BOT_TOKEN=",
462
+ "# SLACK_BOT_TOKEN=",
463
+ "# SLACK_APP_TOKEN=",
464
+ "# TELEGRAM_BOT_TOKEN=",
465
+ "",
466
+ ];
467
+ fs.writeFileSync(envPath, lines.join("\n"));
468
+ result.created = true;
469
+ }
470
+
471
+ return result;
472
+ }
473
+
474
+ /**
475
+ * Ensure openclaw is installed as a dependency.
476
+ */
477
+ function ensureOpenClawDep(cwd) {
478
+ const pkgPath = path.join(cwd, "package.json");
479
+ if (!fs.existsSync(pkgPath)) return { installed: false, reason: "no package.json" };
480
+
481
+ try {
482
+ // Check if already resolvable
483
+ require.resolve("openclaw", { paths: [cwd] });
484
+ return { installed: true, alreadyPresent: true };
485
+ } catch {}
486
+
487
+ // Try npm install
488
+ try {
489
+ console.log(chalk.gray(" Installing openclaw..."));
490
+ execSync("npm install openclaw --save-optional 2>&1", {
491
+ cwd,
492
+ encoding: "utf-8",
493
+ timeout: 120000,
494
+ stdio: ["pipe", "pipe", "pipe"],
495
+ });
496
+ return { installed: true, alreadyPresent: false };
497
+ } catch (err) {
498
+ return {
499
+ installed: false,
500
+ reason: `npm install failed: ${err.message?.split("\n")[0] || "unknown"}`,
501
+ fallback: "Will use npx openclaw (downloads on first run)",
502
+ };
503
+ }
504
+ }
505
+
506
+ // ── Validation ──────────────────────────────────────────────────
507
+
508
+ /**
509
+ * Validate the setup is complete and functional.
510
+ */
511
+ function validate(cwd, env) {
512
+ const checks = [];
513
+
514
+ // Node version
515
+ checks.push({
516
+ name: "Node.js >= 22",
517
+ pass: env.node.ok,
518
+ detail: env.node.ok ? env.node.version : `${env.node.version} (need >= 22)`,
519
+ critical: true,
520
+ });
521
+
522
+ // OpenClaw available
523
+ checks.push({
524
+ name: "OpenClaw",
525
+ pass: env.openclaw.found,
526
+ detail: env.openclaw.found
527
+ ? `${env.openclaw.version || "found"} (${env.openclaw.source})`
528
+ : "not found — will install or use npx",
529
+ critical: false,
530
+ });
531
+
532
+ // API keys
533
+ const hasAnyKey = env.keys.ANTHROPIC_API_KEY.set || env.keys.OPENAI_API_KEY.set || env.keys.WOLVERINE_API_KEY.set;
534
+ checks.push({
535
+ name: "AI API key",
536
+ pass: hasAnyKey,
537
+ detail: hasAnyKey
538
+ ? [
539
+ env.keys.ANTHROPIC_API_KEY.set && "Anthropic",
540
+ env.keys.OPENAI_API_KEY.set && "OpenAI",
541
+ env.keys.WOLVERINE_API_KEY.set && "Wolverine",
542
+ ].filter(Boolean).join(" + ")
543
+ : "none set — add to .env.local",
544
+ critical: false,
545
+ });
546
+
547
+ // Config file
548
+ const configExists = fs.existsSync(path.join(cwd, "wolverine-claw", "config", "settings.json"));
549
+ checks.push({
550
+ name: "Claw config",
551
+ pass: configExists,
552
+ detail: configExists ? "wolverine-claw/config/settings.json" : "missing",
553
+ critical: true,
554
+ });
555
+
556
+ // Entry point
557
+ const indexExists = fs.existsSync(path.join(cwd, "wolverine-claw", "index.js"));
558
+ checks.push({
559
+ name: "Entry point",
560
+ pass: indexExists,
561
+ detail: indexExists ? "wolverine-claw/index.js" : "missing",
562
+ critical: true,
563
+ });
564
+
565
+ // Plugin
566
+ const pluginExists = fs.existsSync(path.join(cwd, "wolverine-claw", "plugins", "wolverine-integration.js"));
567
+ checks.push({
568
+ name: "Wolverine plugin",
569
+ pass: pluginExists,
570
+ detail: pluginExists ? "wolverine-claw/plugins/wolverine-integration.js" : "missing",
571
+ critical: false,
572
+ });
573
+
574
+ // Wolverine core
575
+ checks.push({
576
+ name: "Wolverine core",
577
+ pass: env.wolverine.installed,
578
+ detail: env.wolverine.installed
579
+ ? `v${env.wolverine.version || "?"}`
580
+ : "not detected",
581
+ critical: true,
582
+ });
583
+
584
+ return checks;
585
+ }
586
+
587
+ // ── Main Setup Flow ─────────────────────────────────────────────
588
+
589
+ /**
590
+ * Run the full setup flow. Returns a result object.
591
+ */
592
+ async function setup(cwd, options = {}) {
593
+ const quiet = options.quiet || false;
594
+ const dryRun = options.dryRun || false;
595
+ const forceReinstall = options.force || false;
596
+
597
+ const log = quiet ? () => {} : (...a) => console.log(...a);
598
+ const LINE = "━".repeat(52);
599
+
600
+ log(chalk.blue.bold("\n 🐾 Wolverine Claw — Setup"));
601
+ log(chalk.blue(` ${LINE}\n`));
602
+
603
+ // ── Step 1: Detect environment ──────────────────────────────
604
+ log(chalk.bold(" Detecting environment...\n"));
605
+
606
+ const env = detectEnvironment(cwd);
607
+
608
+ // Node
609
+ const nodeIcon = env.node.ok ? chalk.green(" ✅") : chalk.red(" ❌");
610
+ log(`${nodeIcon} Node.js ${env.node.version}${env.node.ok ? "" : " (need >= 22)"}`);
611
+
612
+ // OS
613
+ log(chalk.gray(` ${env.os.platform}/${env.os.arch} — ${env.os.hostname}`));
614
+
615
+ // OpenClaw
616
+ if (env.openclaw.found) {
617
+ log(chalk.green(` ✅ OpenClaw ${env.openclaw.version || "detected"} (${env.openclaw.source})`));
618
+ if (env.openclaw.configPath) {
619
+ log(chalk.gray(` Config: ${env.openclaw.configPath}`));
620
+ }
621
+ } else {
622
+ log(chalk.yellow(` ⚠️ OpenClaw not found — will install as dependency`));
623
+ }
624
+
625
+ // Wolverine
626
+ if (env.wolverine.installed) {
627
+ log(chalk.green(` ✅ Wolverine v${env.wolverine.version || "?"}`));
628
+ } else {
629
+ log(chalk.yellow(` ⚠️ Wolverine not detected in this directory`));
630
+ }
631
+
632
+ // API keys
633
+ for (const [key, info] of Object.entries(env.keys)) {
634
+ if (key === "WOLVERINE_ADMIN_KEY") continue; // not critical for claw
635
+ if (info.set) {
636
+ log(chalk.green(` ✅ ${key} (${info.source})`));
637
+ } else {
638
+ log(chalk.yellow(` ⚠️ ${key} not set`));
639
+ }
640
+ }
641
+
642
+ // Existing claw
643
+ if (env.existingClaw && !forceReinstall) {
644
+ log(chalk.green(` ✅ wolverine-claw/ already exists`));
645
+ }
646
+
647
+ log("");
648
+
649
+ // ── Step 2: Abort if Node too old ───────────────────────────
650
+ if (!env.node.ok) {
651
+ log(chalk.red(" ❌ Node.js 22+ is required. Please upgrade Node.js.\n"));
652
+ return { success: false, reason: "node-version", env };
653
+ }
654
+
655
+ if (dryRun) {
656
+ log(chalk.gray(" [dry run] Would scaffold wolverine-claw/ here.\n"));
657
+ return { success: true, dryRun: true, env };
658
+ }
659
+
660
+ // ── Step 3: Merge config ────────────────────────────────────
661
+ log(chalk.bold(" Configuring...\n"));
662
+
663
+ // Load default config template
664
+ const defaultConfigPath = path.join(__dirname, "..", "..", "wolverine-claw", "config", "settings.json");
665
+ let defaults;
666
+ try {
667
+ defaults = JSON.parse(fs.readFileSync(defaultConfigPath, "utf-8"));
668
+ } catch {
669
+ // Fallback: build minimal defaults inline
670
+ defaults = buildMinimalDefaults();
671
+ }
672
+
673
+ const mergedConfig = mergeConfig(env.openclaw.config, defaults);
674
+
675
+ if (env.openclaw.config) {
676
+ log(chalk.green(" ✅ Merged OpenClaw config with wolverine defaults"));
677
+ // Log what was imported
678
+ if (env.openclaw.config.gateway?.port) {
679
+ log(chalk.gray(` Gateway port: ${env.openclaw.config.gateway.port}`));
680
+ }
681
+ if (env.openclaw.config.agent?.model || env.openclaw.config.model) {
682
+ log(chalk.gray(` Agent model: ${env.openclaw.config.agent?.model || env.openclaw.config.model}`));
683
+ }
684
+ const importedChannels = env.openclaw.config.channels
685
+ ? Object.keys(env.openclaw.config.channels)
686
+ : [];
687
+ if (importedChannels.length > 0) {
688
+ log(chalk.gray(` Channels: ${importedChannels.join(", ")}`));
689
+ }
690
+ } else {
691
+ log(chalk.gray(" Using wolverine defaults (no OpenClaw config found to merge)"));
692
+ }
693
+
694
+ log("");
695
+
696
+ // ── Step 4: Scaffold ────────────────────────────────────────
697
+ log(chalk.bold(" Scaffolding wolverine-claw/...\n"));
698
+
699
+ const scaffoldResult = scaffold(cwd, mergedConfig, env);
700
+
701
+ for (const f of scaffoldResult.created) {
702
+ log(chalk.green(` + ${f}`));
703
+ }
704
+ for (const f of scaffoldResult.skipped) {
705
+ log(chalk.gray(` ○ ${f}`));
706
+ }
707
+ for (const f of scaffoldResult.errors) {
708
+ log(chalk.red(` ✗ ${f}`));
709
+ }
710
+
711
+ log("");
712
+
713
+ // ── Step 5: Environment file ────────────────────────────────
714
+ log(chalk.bold(" Environment...\n"));
715
+
716
+ const envResult = ensureEnvFile(cwd, env);
717
+ if (envResult.created) {
718
+ log(chalk.green(" + .env.local created (add your API keys)"));
719
+ } else {
720
+ log(chalk.gray(" ○ .env.local exists"));
721
+ if (envResult.keysAdded.length > 0) {
722
+ log(chalk.green(` + Added ${envResult.keysAdded.length} claw key templates`));
723
+ }
724
+ }
725
+
726
+ log("");
727
+
728
+ // ── Step 6: Install OpenClaw ────────────────────────────────
729
+ if (!env.openclaw.localInstall) {
730
+ log(chalk.bold(" Dependencies...\n"));
731
+ const depResult = ensureOpenClawDep(cwd);
732
+ if (depResult.installed) {
733
+ log(chalk.green(` ✅ openclaw ${depResult.alreadyPresent ? "already installed" : "installed"}`));
734
+ } else {
735
+ log(chalk.yellow(` ⚠️ openclaw: ${depResult.reason}`));
736
+ if (depResult.fallback) {
737
+ log(chalk.gray(` ${depResult.fallback}`));
738
+ }
739
+ }
740
+ log("");
741
+ }
742
+
743
+ // ── Step 7: Validate ───────────────────────────────────────
744
+ log(chalk.bold(" Validating...\n"));
745
+
746
+ // Re-detect after setup
747
+ const postEnv = detectEnvironment(cwd);
748
+ const checks = validate(cwd, postEnv);
749
+
750
+ let allCriticalPass = true;
751
+ for (const check of checks) {
752
+ const icon = check.pass ? chalk.green(" ✅") : (check.critical ? chalk.red(" ❌") : chalk.yellow(" ⚠️ "));
753
+ log(`${icon} ${check.name}: ${check.detail}`);
754
+ if (check.critical && !check.pass) allCriticalPass = false;
755
+ }
756
+
757
+ log("");
758
+
759
+ // ── Step 8: Next steps ─────────────────────────────────────
760
+ log(chalk.blue(` ${LINE}`));
761
+
762
+ if (allCriticalPass) {
763
+ log(chalk.green.bold("\n ✅ Wolverine Claw is ready!\n"));
764
+ } else {
765
+ log(chalk.yellow.bold("\n ⚠️ Setup completed with warnings.\n"));
766
+ }
767
+
768
+ log(chalk.bold(" Next steps:\n"));
769
+
770
+ // Check what the user still needs to do
771
+ const todos = [];
772
+
773
+ if (!postEnv.keys.ANTHROPIC_API_KEY.set && !postEnv.keys.OPENAI_API_KEY.set) {
774
+ todos.push({
775
+ step: "Add API keys to .env.local",
776
+ cmd: null,
777
+ detail: "ANTHROPIC_API_KEY or OPENAI_API_KEY required for AI healing",
778
+ });
779
+ }
780
+
781
+ todos.push({
782
+ step: "Start Wolverine Claw",
783
+ cmd: "npm run claw",
784
+ detail: "Launches OpenClaw gateway with wolverine self-healing",
785
+ });
786
+
787
+ todos.push({
788
+ step: "Check configuration",
789
+ cmd: "npm run claw:info",
790
+ detail: "Shows current config, channels, and API key status",
791
+ });
792
+
793
+ todos.push({
794
+ step: "Enable channels (optional)",
795
+ cmd: null,
796
+ detail: "Edit wolverine-claw/config/settings.json → channels section",
797
+ });
798
+
799
+ for (let i = 0; i < todos.length; i++) {
800
+ const t = todos[i];
801
+ log(chalk.white(` ${i + 1}. ${t.step}`));
802
+ if (t.cmd) {
803
+ log(chalk.cyan(` $ ${t.cmd}`));
804
+ }
805
+ if (t.detail) {
806
+ log(chalk.gray(` ${t.detail}`));
807
+ }
808
+ }
809
+
810
+ log("");
811
+ log(chalk.gray(" Config: wolverine-claw/config/settings.json"));
812
+ log(chalk.gray(" Secrets: .env.local"));
813
+ log(chalk.gray(" Docs: https://github.com/bobbyswhip/Wolverine"));
814
+ log("");
815
+
816
+ return {
817
+ success: allCriticalPass,
818
+ env: postEnv,
819
+ scaffoldResult,
820
+ checks,
821
+ todos,
822
+ };
823
+ }
824
+
825
+ /**
826
+ * Build minimal default config when template file isn't available.
827
+ */
828
+ function buildMinimalDefaults() {
829
+ return {
830
+ "_": "Wolverine Claw Configuration",
831
+ gateway: { port: 18789, host: "127.0.0.1" },
832
+ agent: { model: "claude-sonnet-4-6", maxTurns: 25, timeoutMs: 120000 },
833
+ models: {
834
+ reasoning: "claude-sonnet-4-6",
835
+ coding: "claude-sonnet-4-6",
836
+ chat: "claude-sonnet-4-6",
837
+ embedding: "text-embedding-3-small",
838
+ },
839
+ channels: { terminal: { enabled: true } },
840
+ healing: {
841
+ enabled: true,
842
+ healTimeoutMs: 300000,
843
+ maxHealsPerWindow: 5,
844
+ windowMs: 300000,
845
+ loopMaxAttempts: 3,
846
+ loopWindowMs: 600000,
847
+ },
848
+ skills: {
849
+ codingAgent: { enabled: true, defaultAgent: "pi", sandbox: true, allowedPaths: ["wolverine-claw/workspace/"] },
850
+ browserControl: { enabled: false },
851
+ cron: { enabled: true, maxJobs: 10 },
852
+ canvas: { enabled: false },
853
+ },
854
+ workspace: { path: "wolverine-claw/workspace", maxFileSizeMB: 50, allowedExtensions: ["*"] },
855
+ security: { dmPairing: true, sandbox: true, blockedCommands: ["rm -rf /", "format", "shutdown", "reboot"], maxConcurrentSessions: 5 },
856
+ logging: { level: "info", logFile: ".wolverine/claw.log", maxLogSizeMB: 50 },
857
+ remoteAccess: { enabled: false, method: "tailscale" },
858
+ backup: { enabled: true, stabilityMs: 1800000, retentionDays: 7 },
859
+ };
860
+ }
861
+
862
+ module.exports = {
863
+ setup,
864
+ detectEnvironment,
865
+ detectOpenClaw,
866
+ mergeConfig,
867
+ scaffold,
868
+ validate,
869
+ ensureEnvFile,
870
+ ensureOpenClawDep,
871
+ };