wolverine-ai 6.1.2 → 6.2.1
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/README.md +171 -0
- package/bin/wolverine-claw.js +184 -0
- package/bin/wolverine.js +27 -0
- package/package.json +10 -4
- package/src/brain/brain.js +20 -0
- package/src/claw/claw-runner.js +422 -0
- package/src/claw/setup.js +876 -0
- package/src/claw/standalone-agent.js +488 -0
- package/wolverine-claw/config/settings.json +115 -0
- package/wolverine-claw/index.js +322 -0
- package/wolverine-claw/plugins/wolverine-integration.js +283 -0
- package/wolverine-claw/skills/.gitkeep +0 -0
- package/wolverine-claw/workspace/.gitignore +5 -0
- package/wolverine-claw/workspace/.gitkeep +0 -0
|
@@ -0,0 +1,876 @@
|
|
|
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 — not critical since standalone agent works on Node 20+
|
|
515
|
+
const nodeMinOk = env.node.major >= 20;
|
|
516
|
+
checks.push({
|
|
517
|
+
name: "Node.js",
|
|
518
|
+
pass: nodeMinOk,
|
|
519
|
+
detail: env.node.ok
|
|
520
|
+
? `${env.node.version} (full OpenClaw support)`
|
|
521
|
+
: nodeMinOk
|
|
522
|
+
? `${env.node.version} (standalone agent mode — upgrade to 22+ for OpenClaw)`
|
|
523
|
+
: `${env.node.version} (need >= 20)`,
|
|
524
|
+
critical: !nodeMinOk,
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
// OpenClaw available
|
|
528
|
+
checks.push({
|
|
529
|
+
name: "OpenClaw",
|
|
530
|
+
pass: env.openclaw.found,
|
|
531
|
+
detail: env.openclaw.found
|
|
532
|
+
? `${env.openclaw.version || "found"} (${env.openclaw.source})`
|
|
533
|
+
: "not found — will install or use npx",
|
|
534
|
+
critical: false,
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
// API keys
|
|
538
|
+
const hasAnyKey = env.keys.ANTHROPIC_API_KEY.set || env.keys.OPENAI_API_KEY.set || env.keys.WOLVERINE_API_KEY.set;
|
|
539
|
+
checks.push({
|
|
540
|
+
name: "AI API key",
|
|
541
|
+
pass: hasAnyKey,
|
|
542
|
+
detail: hasAnyKey
|
|
543
|
+
? [
|
|
544
|
+
env.keys.ANTHROPIC_API_KEY.set && "Anthropic",
|
|
545
|
+
env.keys.OPENAI_API_KEY.set && "OpenAI",
|
|
546
|
+
env.keys.WOLVERINE_API_KEY.set && "Wolverine",
|
|
547
|
+
].filter(Boolean).join(" + ")
|
|
548
|
+
: "none set — add to .env.local",
|
|
549
|
+
critical: false,
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
// Config file
|
|
553
|
+
const configExists = fs.existsSync(path.join(cwd, "wolverine-claw", "config", "settings.json"));
|
|
554
|
+
checks.push({
|
|
555
|
+
name: "Claw config",
|
|
556
|
+
pass: configExists,
|
|
557
|
+
detail: configExists ? "wolverine-claw/config/settings.json" : "missing",
|
|
558
|
+
critical: true,
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
// Entry point
|
|
562
|
+
const indexExists = fs.existsSync(path.join(cwd, "wolverine-claw", "index.js"));
|
|
563
|
+
checks.push({
|
|
564
|
+
name: "Entry point",
|
|
565
|
+
pass: indexExists,
|
|
566
|
+
detail: indexExists ? "wolverine-claw/index.js" : "missing",
|
|
567
|
+
critical: true,
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
// Plugin
|
|
571
|
+
const pluginExists = fs.existsSync(path.join(cwd, "wolverine-claw", "plugins", "wolverine-integration.js"));
|
|
572
|
+
checks.push({
|
|
573
|
+
name: "Wolverine plugin",
|
|
574
|
+
pass: pluginExists,
|
|
575
|
+
detail: pluginExists ? "wolverine-claw/plugins/wolverine-integration.js" : "missing",
|
|
576
|
+
critical: false,
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
// Wolverine core
|
|
580
|
+
checks.push({
|
|
581
|
+
name: "Wolverine core",
|
|
582
|
+
pass: env.wolverine.installed,
|
|
583
|
+
detail: env.wolverine.installed
|
|
584
|
+
? `v${env.wolverine.version || "?"}`
|
|
585
|
+
: "not detected",
|
|
586
|
+
critical: true,
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
return checks;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// ── Main Setup Flow ─────────────────────────────────────────────
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Run the full setup flow. Returns a result object.
|
|
596
|
+
*/
|
|
597
|
+
async function setup(cwd, options = {}) {
|
|
598
|
+
const quiet = options.quiet || false;
|
|
599
|
+
const dryRun = options.dryRun || false;
|
|
600
|
+
const forceReinstall = options.force || false;
|
|
601
|
+
|
|
602
|
+
const log = quiet ? () => {} : (...a) => console.log(...a);
|
|
603
|
+
const LINE = "━".repeat(52);
|
|
604
|
+
|
|
605
|
+
log(chalk.blue.bold("\n 🐾 Wolverine Claw — Setup"));
|
|
606
|
+
log(chalk.blue(` ${LINE}\n`));
|
|
607
|
+
|
|
608
|
+
// ── Step 1: Detect environment ──────────────────────────────
|
|
609
|
+
log(chalk.bold(" Detecting environment...\n"));
|
|
610
|
+
|
|
611
|
+
const env = detectEnvironment(cwd);
|
|
612
|
+
|
|
613
|
+
// Node
|
|
614
|
+
const nodeIcon = env.node.ok ? chalk.green(" ✅") : chalk.red(" ❌");
|
|
615
|
+
log(`${nodeIcon} Node.js ${env.node.version}${env.node.ok ? "" : " (need >= 22)"}`);
|
|
616
|
+
|
|
617
|
+
// OS
|
|
618
|
+
log(chalk.gray(` ${env.os.platform}/${env.os.arch} — ${env.os.hostname}`));
|
|
619
|
+
|
|
620
|
+
// OpenClaw
|
|
621
|
+
if (env.openclaw.found) {
|
|
622
|
+
log(chalk.green(` ✅ OpenClaw ${env.openclaw.version || "detected"} (${env.openclaw.source})`));
|
|
623
|
+
if (env.openclaw.configPath) {
|
|
624
|
+
log(chalk.gray(` Config: ${env.openclaw.configPath}`));
|
|
625
|
+
}
|
|
626
|
+
} else {
|
|
627
|
+
log(chalk.yellow(` ⚠️ OpenClaw not found — will install as dependency`));
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Wolverine
|
|
631
|
+
if (env.wolverine.installed) {
|
|
632
|
+
log(chalk.green(` ✅ Wolverine v${env.wolverine.version || "?"}`));
|
|
633
|
+
} else {
|
|
634
|
+
log(chalk.yellow(` ⚠️ Wolverine not detected in this directory`));
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// API keys
|
|
638
|
+
for (const [key, info] of Object.entries(env.keys)) {
|
|
639
|
+
if (key === "WOLVERINE_ADMIN_KEY") continue; // not critical for claw
|
|
640
|
+
if (info.set) {
|
|
641
|
+
log(chalk.green(` ✅ ${key} (${info.source})`));
|
|
642
|
+
} else {
|
|
643
|
+
log(chalk.yellow(` ⚠️ ${key} not set`));
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Existing claw
|
|
648
|
+
if (env.existingClaw && !forceReinstall) {
|
|
649
|
+
log(chalk.green(` ✅ wolverine-claw/ already exists`));
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
log("");
|
|
653
|
+
|
|
654
|
+
// ── Step 2: Node version check ──────────────────────────────
|
|
655
|
+
if (!env.node.ok) {
|
|
656
|
+
log(chalk.yellow(" ⚠️ Node.js 22+ recommended (required for OpenClaw multi-channel)."));
|
|
657
|
+
log(chalk.gray(" Standalone agent mode works on Node 20+.\n"));
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (dryRun) {
|
|
661
|
+
log(chalk.gray(" [dry run] Would scaffold wolverine-claw/ here.\n"));
|
|
662
|
+
return { success: true, dryRun: true, env };
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// ── Step 3: Merge config ────────────────────────────────────
|
|
666
|
+
log(chalk.bold(" Configuring...\n"));
|
|
667
|
+
|
|
668
|
+
// Load default config template
|
|
669
|
+
const defaultConfigPath = path.join(__dirname, "..", "..", "wolverine-claw", "config", "settings.json");
|
|
670
|
+
let defaults;
|
|
671
|
+
try {
|
|
672
|
+
defaults = JSON.parse(fs.readFileSync(defaultConfigPath, "utf-8"));
|
|
673
|
+
} catch {
|
|
674
|
+
// Fallback: build minimal defaults inline
|
|
675
|
+
defaults = buildMinimalDefaults();
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const mergedConfig = mergeConfig(env.openclaw.config, defaults);
|
|
679
|
+
|
|
680
|
+
if (env.openclaw.config) {
|
|
681
|
+
log(chalk.green(" ✅ Merged OpenClaw config with wolverine defaults"));
|
|
682
|
+
// Log what was imported
|
|
683
|
+
if (env.openclaw.config.gateway?.port) {
|
|
684
|
+
log(chalk.gray(` Gateway port: ${env.openclaw.config.gateway.port}`));
|
|
685
|
+
}
|
|
686
|
+
if (env.openclaw.config.agent?.model || env.openclaw.config.model) {
|
|
687
|
+
log(chalk.gray(` Agent model: ${env.openclaw.config.agent?.model || env.openclaw.config.model}`));
|
|
688
|
+
}
|
|
689
|
+
const importedChannels = env.openclaw.config.channels
|
|
690
|
+
? Object.keys(env.openclaw.config.channels)
|
|
691
|
+
: [];
|
|
692
|
+
if (importedChannels.length > 0) {
|
|
693
|
+
log(chalk.gray(` Channels: ${importedChannels.join(", ")}`));
|
|
694
|
+
}
|
|
695
|
+
} else {
|
|
696
|
+
log(chalk.gray(" Using wolverine defaults (no OpenClaw config found to merge)"));
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
log("");
|
|
700
|
+
|
|
701
|
+
// ── Step 4: Scaffold ────────────────────────────────────────
|
|
702
|
+
log(chalk.bold(" Scaffolding wolverine-claw/...\n"));
|
|
703
|
+
|
|
704
|
+
const scaffoldResult = scaffold(cwd, mergedConfig, env);
|
|
705
|
+
|
|
706
|
+
for (const f of scaffoldResult.created) {
|
|
707
|
+
log(chalk.green(` + ${f}`));
|
|
708
|
+
}
|
|
709
|
+
for (const f of scaffoldResult.skipped) {
|
|
710
|
+
log(chalk.gray(` ○ ${f}`));
|
|
711
|
+
}
|
|
712
|
+
for (const f of scaffoldResult.errors) {
|
|
713
|
+
log(chalk.red(` ✗ ${f}`));
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
log("");
|
|
717
|
+
|
|
718
|
+
// ── Step 5: Environment file ────────────────────────────────
|
|
719
|
+
log(chalk.bold(" Environment...\n"));
|
|
720
|
+
|
|
721
|
+
const envResult = ensureEnvFile(cwd, env);
|
|
722
|
+
if (envResult.created) {
|
|
723
|
+
log(chalk.green(" + .env.local created (add your API keys)"));
|
|
724
|
+
} else {
|
|
725
|
+
log(chalk.gray(" ○ .env.local exists"));
|
|
726
|
+
if (envResult.keysAdded.length > 0) {
|
|
727
|
+
log(chalk.green(` + Added ${envResult.keysAdded.length} claw key templates`));
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
log("");
|
|
732
|
+
|
|
733
|
+
// ── Step 6: Install OpenClaw ────────────────────────────────
|
|
734
|
+
if (!env.openclaw.localInstall) {
|
|
735
|
+
log(chalk.bold(" Dependencies...\n"));
|
|
736
|
+
const depResult = ensureOpenClawDep(cwd);
|
|
737
|
+
if (depResult.installed) {
|
|
738
|
+
log(chalk.green(` ✅ openclaw ${depResult.alreadyPresent ? "already installed" : "installed"}`));
|
|
739
|
+
} else {
|
|
740
|
+
log(chalk.yellow(` ⚠️ openclaw: ${depResult.reason}`));
|
|
741
|
+
if (depResult.fallback) {
|
|
742
|
+
log(chalk.gray(` ${depResult.fallback}`));
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
log("");
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// ── Step 7: Validate ───────────────────────────────────────
|
|
749
|
+
log(chalk.bold(" Validating...\n"));
|
|
750
|
+
|
|
751
|
+
// Re-detect after setup
|
|
752
|
+
const postEnv = detectEnvironment(cwd);
|
|
753
|
+
const checks = validate(cwd, postEnv);
|
|
754
|
+
|
|
755
|
+
let allCriticalPass = true;
|
|
756
|
+
for (const check of checks) {
|
|
757
|
+
const icon = check.pass ? chalk.green(" ✅") : (check.critical ? chalk.red(" ❌") : chalk.yellow(" ⚠️ "));
|
|
758
|
+
log(`${icon} ${check.name}: ${check.detail}`);
|
|
759
|
+
if (check.critical && !check.pass) allCriticalPass = false;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
log("");
|
|
763
|
+
|
|
764
|
+
// ── Step 8: Next steps ─────────────────────────────────────
|
|
765
|
+
log(chalk.blue(` ${LINE}`));
|
|
766
|
+
|
|
767
|
+
if (allCriticalPass) {
|
|
768
|
+
log(chalk.green.bold("\n ✅ Wolverine Claw is ready!\n"));
|
|
769
|
+
} else {
|
|
770
|
+
log(chalk.yellow.bold("\n ⚠️ Setup completed with warnings.\n"));
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
log(chalk.bold(" Next steps:\n"));
|
|
774
|
+
|
|
775
|
+
// Check what the user still needs to do
|
|
776
|
+
const todos = [];
|
|
777
|
+
|
|
778
|
+
if (!postEnv.keys.ANTHROPIC_API_KEY.set && !postEnv.keys.OPENAI_API_KEY.set) {
|
|
779
|
+
todos.push({
|
|
780
|
+
step: "Add API keys to .env.local",
|
|
781
|
+
cmd: null,
|
|
782
|
+
detail: "ANTHROPIC_API_KEY or OPENAI_API_KEY required for AI healing",
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
todos.push({
|
|
787
|
+
step: "Start Wolverine Claw",
|
|
788
|
+
cmd: "npm run claw",
|
|
789
|
+
detail: "Launches OpenClaw gateway with wolverine self-healing",
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
todos.push({
|
|
793
|
+
step: "Check configuration",
|
|
794
|
+
cmd: "npm run claw:info",
|
|
795
|
+
detail: "Shows current config, channels, and API key status",
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
todos.push({
|
|
799
|
+
step: "Enable channels (optional)",
|
|
800
|
+
cmd: null,
|
|
801
|
+
detail: "Edit wolverine-claw/config/settings.json → channels section",
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
for (let i = 0; i < todos.length; i++) {
|
|
805
|
+
const t = todos[i];
|
|
806
|
+
log(chalk.white(` ${i + 1}. ${t.step}`));
|
|
807
|
+
if (t.cmd) {
|
|
808
|
+
log(chalk.cyan(` $ ${t.cmd}`));
|
|
809
|
+
}
|
|
810
|
+
if (t.detail) {
|
|
811
|
+
log(chalk.gray(` ${t.detail}`));
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
log("");
|
|
816
|
+
log(chalk.gray(" Config: wolverine-claw/config/settings.json"));
|
|
817
|
+
log(chalk.gray(" Secrets: .env.local"));
|
|
818
|
+
log(chalk.gray(" Docs: https://github.com/bobbyswhip/Wolverine"));
|
|
819
|
+
log("");
|
|
820
|
+
|
|
821
|
+
return {
|
|
822
|
+
success: allCriticalPass,
|
|
823
|
+
env: postEnv,
|
|
824
|
+
scaffoldResult,
|
|
825
|
+
checks,
|
|
826
|
+
todos,
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Build minimal default config when template file isn't available.
|
|
832
|
+
*/
|
|
833
|
+
function buildMinimalDefaults() {
|
|
834
|
+
return {
|
|
835
|
+
"_": "Wolverine Claw Configuration",
|
|
836
|
+
gateway: { port: 18789, host: "127.0.0.1" },
|
|
837
|
+
agent: { model: "claude-sonnet-4-6", maxTurns: 25, timeoutMs: 120000 },
|
|
838
|
+
models: {
|
|
839
|
+
reasoning: "claude-sonnet-4-6",
|
|
840
|
+
coding: "claude-sonnet-4-6",
|
|
841
|
+
chat: "claude-sonnet-4-6",
|
|
842
|
+
embedding: "text-embedding-3-small",
|
|
843
|
+
},
|
|
844
|
+
channels: { terminal: { enabled: true } },
|
|
845
|
+
healing: {
|
|
846
|
+
enabled: true,
|
|
847
|
+
healTimeoutMs: 300000,
|
|
848
|
+
maxHealsPerWindow: 5,
|
|
849
|
+
windowMs: 300000,
|
|
850
|
+
loopMaxAttempts: 3,
|
|
851
|
+
loopWindowMs: 600000,
|
|
852
|
+
},
|
|
853
|
+
skills: {
|
|
854
|
+
codingAgent: { enabled: true, defaultAgent: "pi", sandbox: true, allowedPaths: ["wolverine-claw/workspace/"] },
|
|
855
|
+
browserControl: { enabled: false },
|
|
856
|
+
cron: { enabled: true, maxJobs: 10 },
|
|
857
|
+
canvas: { enabled: false },
|
|
858
|
+
},
|
|
859
|
+
workspace: { path: "wolverine-claw/workspace", maxFileSizeMB: 50, allowedExtensions: ["*"] },
|
|
860
|
+
security: { dmPairing: true, sandbox: true, blockedCommands: ["rm -rf /", "format", "shutdown", "reboot"], maxConcurrentSessions: 5 },
|
|
861
|
+
logging: { level: "info", logFile: ".wolverine/claw.log", maxLogSizeMB: 50 },
|
|
862
|
+
remoteAccess: { enabled: false, method: "tailscale" },
|
|
863
|
+
backup: { enabled: true, stabilityMs: 1800000, retentionDays: 7 },
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
module.exports = {
|
|
868
|
+
setup,
|
|
869
|
+
detectEnvironment,
|
|
870
|
+
detectOpenClaw,
|
|
871
|
+
mergeConfig,
|
|
872
|
+
scaffold,
|
|
873
|
+
validate,
|
|
874
|
+
ensureEnvFile,
|
|
875
|
+
ensureOpenClawDep,
|
|
876
|
+
};
|