wolverine-ai 2.4.4 → 2.5.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/package.json +1 -1
- package/server/config/settings.json +1 -1
- package/src/brain/brain.js +49 -1
- package/src/platform/auto-update.js +123 -25
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wolverine-ai",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "Self-healing Node.js server framework powered by AI. Catches crashes, diagnoses errors, generates fixes, verifies, and restarts — automatically.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
package/src/brain/brain.js
CHANGED
|
@@ -263,10 +263,17 @@ class Brain {
|
|
|
263
263
|
|
|
264
264
|
console.log(chalk.gray(` 🧠 Brain: ${stats.totalEntries} memories loaded`));
|
|
265
265
|
|
|
266
|
-
// 1. Seed wolverine docs on first run
|
|
266
|
+
// 1. Seed wolverine docs on first run OR merge new seeds after framework update
|
|
267
|
+
const seedRefreshPath = path.join(this.projectRoot, ".wolverine", "brain", ".seed-refresh");
|
|
268
|
+
const needsSeedRefresh = fs.existsSync(seedRefreshPath);
|
|
269
|
+
|
|
267
270
|
if (isFirstRun) {
|
|
268
271
|
console.log(chalk.gray(" 🧠 First run — seeding wolverine documentation..."));
|
|
269
272
|
await this._seedDocs();
|
|
273
|
+
} else if (needsSeedRefresh) {
|
|
274
|
+
console.log(chalk.gray(" 🧠 Framework updated — merging new seed docs..."));
|
|
275
|
+
await this._mergeSeedDocs();
|
|
276
|
+
try { fs.unlinkSync(seedRefreshPath); } catch {}
|
|
270
277
|
}
|
|
271
278
|
|
|
272
279
|
// 2. Scan project for live function map
|
|
@@ -394,6 +401,47 @@ class Brain {
|
|
|
394
401
|
console.log(chalk.gray(` 🧠 Seeded ${SEED_DOCS.length} documentation entries`));
|
|
395
402
|
}
|
|
396
403
|
|
|
404
|
+
/**
|
|
405
|
+
* Merge new seed docs into existing brain — append only, never delete.
|
|
406
|
+
* Compares by topic metadata to find new/updated docs.
|
|
407
|
+
* Existing memories (errors, fixes, learnings) are untouched.
|
|
408
|
+
*/
|
|
409
|
+
async _mergeSeedDocs() {
|
|
410
|
+
const existing = this.store.getNamespace("docs") || [];
|
|
411
|
+
const existingTopics = new Set(existing.map(e => e.metadata?.topic).filter(Boolean));
|
|
412
|
+
|
|
413
|
+
// Find seed docs whose topic isn't already in the brain
|
|
414
|
+
const newDocs = SEED_DOCS.filter(d => !existingTopics.has(d.metadata?.topic));
|
|
415
|
+
// Find seed docs whose topic exists but text has changed (updated knowledge)
|
|
416
|
+
const updatedDocs = SEED_DOCS.filter(d => {
|
|
417
|
+
if (!existingTopics.has(d.metadata?.topic)) return false;
|
|
418
|
+
const match = existing.find(e => e.metadata?.topic === d.metadata?.topic);
|
|
419
|
+
return match && match.text !== d.text;
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
const toEmbed = [...newDocs, ...updatedDocs];
|
|
423
|
+
if (toEmbed.length === 0) {
|
|
424
|
+
console.log(chalk.gray(" 🧠 Brain seeds already up to date"));
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Remove old versions of updated docs
|
|
429
|
+
for (const doc of updatedDocs) {
|
|
430
|
+
const old = existing.find(e => e.metadata?.topic === doc.metadata?.topic);
|
|
431
|
+
if (old) this.store.delete(old.id);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Embed and add new/updated docs
|
|
435
|
+
const texts = toEmbed.map(d => d.text);
|
|
436
|
+
const embeddings = await embedBatch(texts);
|
|
437
|
+
for (let i = 0; i < toEmbed.length; i++) {
|
|
438
|
+
this.store.add("docs", toEmbed[i].text, embeddings[i], toEmbed[i].metadata);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
this.store.save();
|
|
442
|
+
console.log(chalk.gray(` 🧠 Merged: ${newDocs.length} new + ${updatedDocs.length} updated seed docs`));
|
|
443
|
+
}
|
|
444
|
+
|
|
397
445
|
async _embedFunctionMap() {
|
|
398
446
|
// Clear old function map entries
|
|
399
447
|
const oldEntries = this.store.getNamespace("functions");
|
|
@@ -91,25 +91,70 @@ function isNewer(latest, current) {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
/**
|
|
94
|
-
* Protect
|
|
94
|
+
* Protect ALL user files before update and restore after.
|
|
95
|
+
* The entire server/ directory is sacred — auto-update must never touch it.
|
|
96
|
+
* Also protects .env files and any user config.
|
|
95
97
|
*/
|
|
96
|
-
function
|
|
97
|
-
const configs = [
|
|
98
|
-
"server/config/settings.json",
|
|
99
|
-
".env.local",
|
|
100
|
-
".env",
|
|
101
|
-
];
|
|
98
|
+
function backupUserFiles(cwd) {
|
|
102
99
|
const backups = {};
|
|
103
|
-
|
|
100
|
+
|
|
101
|
+
// Protect config files
|
|
102
|
+
const protectedFiles = [".env.local", ".env"];
|
|
103
|
+
|
|
104
|
+
// Protect ALL .wolverine/ state (backups, brain, events, usage, repairs)
|
|
105
|
+
const wolvDir = path.join(cwd, ".wolverine");
|
|
106
|
+
if (fs.existsSync(wolvDir)) {
|
|
107
|
+
const walkState = (dir, base) => {
|
|
108
|
+
try {
|
|
109
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
110
|
+
for (const entry of entries) {
|
|
111
|
+
const fullPath = path.join(dir, entry.name);
|
|
112
|
+
const relPath = path.join(base, entry.name).replace(/\\/g, "/");
|
|
113
|
+
if (entry.isDirectory()) { walkState(fullPath, relPath); }
|
|
114
|
+
else {
|
|
115
|
+
try {
|
|
116
|
+
const stat = fs.statSync(fullPath);
|
|
117
|
+
if (stat.size <= 10 * 1024 * 1024) { // skip files > 10MB
|
|
118
|
+
backups[relPath] = fs.readFileSync(fullPath, "utf-8");
|
|
119
|
+
}
|
|
120
|
+
} catch {}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} catch {}
|
|
124
|
+
};
|
|
125
|
+
walkState(wolvDir, ".wolverine");
|
|
126
|
+
}
|
|
127
|
+
for (const file of protectedFiles) {
|
|
104
128
|
const fullPath = path.join(cwd, file);
|
|
105
129
|
if (fs.existsSync(fullPath)) {
|
|
106
130
|
backups[file] = fs.readFileSync(fullPath, "utf-8");
|
|
107
131
|
}
|
|
108
132
|
}
|
|
133
|
+
|
|
134
|
+
// Protect entire server/ directory (recursive)
|
|
135
|
+
const serverDir = path.join(cwd, "server");
|
|
136
|
+
if (fs.existsSync(serverDir)) {
|
|
137
|
+
const walk = (dir, base) => {
|
|
138
|
+
try {
|
|
139
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
140
|
+
for (const entry of entries) {
|
|
141
|
+
if (entry.name === "node_modules") continue;
|
|
142
|
+
const fullPath = path.join(dir, entry.name);
|
|
143
|
+
const relPath = path.join(base, entry.name).replace(/\\/g, "/");
|
|
144
|
+
if (entry.isDirectory()) { walk(fullPath, relPath); }
|
|
145
|
+
else {
|
|
146
|
+
try { backups[relPath] = fs.readFileSync(fullPath, "utf-8"); } catch {}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
} catch {}
|
|
150
|
+
};
|
|
151
|
+
walk(serverDir, "server");
|
|
152
|
+
}
|
|
153
|
+
|
|
109
154
|
return backups;
|
|
110
155
|
}
|
|
111
156
|
|
|
112
|
-
function
|
|
157
|
+
function restoreUserFiles(cwd, backups) {
|
|
113
158
|
for (const [file, content] of Object.entries(backups)) {
|
|
114
159
|
const fullPath = path.join(cwd, file);
|
|
115
160
|
try {
|
|
@@ -144,33 +189,41 @@ function upgrade(cwd, logger) {
|
|
|
144
189
|
console.log(chalk.blue(`\n 🔄 Wolverine update available: ${current} → ${latest}`));
|
|
145
190
|
if (logger) logger.info("update.start", `Upgrading ${current} → ${latest}`, { from: current, to: latest });
|
|
146
191
|
|
|
147
|
-
// Back up configs
|
|
148
|
-
const
|
|
149
|
-
console.log(chalk.gray(` 🔒 Backed up ${Object.keys(
|
|
192
|
+
// Back up ALL user files (server/, .env, configs)
|
|
193
|
+
const userBackups = backupUserFiles(cwd);
|
|
194
|
+
console.log(chalk.gray(` 🔒 Backed up ${Object.keys(userBackups).length} user files (server/ protected)`));
|
|
150
195
|
|
|
151
196
|
try {
|
|
152
|
-
// Detect install method: git clone or npm package
|
|
153
197
|
const useGit = isGitRepo(cwd);
|
|
154
|
-
let cmd;
|
|
155
198
|
|
|
156
199
|
if (useGit) {
|
|
157
|
-
// Git-cloned:
|
|
158
|
-
|
|
159
|
-
console.log(chalk.blue(` 📦 Git repo
|
|
200
|
+
// Git-cloned: ONLY update framework files, NEVER touch server/
|
|
201
|
+
// Fetch latest, then selectively checkout only framework dirs
|
|
202
|
+
console.log(chalk.blue(` 📦 Git repo — selective framework update (server/ untouched)`));
|
|
203
|
+
execSync("git fetch origin master", { cwd, stdio: "pipe", timeout: 30000 });
|
|
204
|
+
// Only update: src/, bin/, package.json, examples/, tests/, CLAUDE.md, README.md, CHANGELOG.md
|
|
205
|
+
const frameworkPaths = "src/ bin/ package.json package-lock.json examples/ tests/ CLAUDE.md README.md CHANGELOG.md .npmignore";
|
|
206
|
+
execSync(`git checkout origin/master -- ${frameworkPaths}`, { cwd, stdio: "pipe", timeout: 30000 });
|
|
207
|
+
execSync("npm install", { cwd, stdio: "pipe", timeout: 120000 });
|
|
160
208
|
} else {
|
|
161
209
|
// npm-installed: update the package
|
|
162
210
|
const isGlobal = __dirname.includes("node_modules") && !cwd.includes("node_modules");
|
|
163
|
-
cmd = isGlobal
|
|
211
|
+
const cmd = isGlobal
|
|
164
212
|
? `npm install -g ${PACKAGE_NAME}@${latest}`
|
|
165
213
|
: `npm install ${PACKAGE_NAME}@${latest}`;
|
|
214
|
+
console.log(chalk.blue(` 📦 Running: ${cmd}`));
|
|
215
|
+
execSync(cmd, { cwd, stdio: "pipe", timeout: 120000 });
|
|
166
216
|
}
|
|
167
217
|
|
|
168
|
-
|
|
169
|
-
|
|
218
|
+
// Restore ALL user files (server/, .env, .wolverine/) — belt AND suspenders
|
|
219
|
+
restoreUserFiles(cwd, userBackups);
|
|
220
|
+
console.log(chalk.gray(` 🔒 Restored ${Object.keys(userBackups).length} user files`));
|
|
170
221
|
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
|
|
222
|
+
// Merge new brain seed docs into existing brain (append, don't replace)
|
|
223
|
+
try {
|
|
224
|
+
const brainMerged = _mergeBrainSeeds(cwd);
|
|
225
|
+
if (brainMerged > 0) console.log(chalk.gray(` 🧠 Merged ${brainMerged} new seed docs into brain`));
|
|
226
|
+
} catch {}
|
|
174
227
|
|
|
175
228
|
// Clear version cache
|
|
176
229
|
_currentVersion = null;
|
|
@@ -180,8 +233,8 @@ function upgrade(cwd, logger) {
|
|
|
180
233
|
|
|
181
234
|
return { success: true, from: current, to: latest };
|
|
182
235
|
} catch (err) {
|
|
183
|
-
// Restore
|
|
184
|
-
|
|
236
|
+
// Restore user files on failure
|
|
237
|
+
restoreUserFiles(cwd, userBackups);
|
|
185
238
|
const errMsg = (err.message || "").slice(0, 100);
|
|
186
239
|
console.log(chalk.yellow(` ⚠️ Update failed: ${errMsg}`));
|
|
187
240
|
if (logger) logger.warn("update.failed", `Upgrade failed: ${errMsg}`, { from: current, to: latest });
|
|
@@ -211,6 +264,51 @@ function checkForUpdate(cwd) {
|
|
|
211
264
|
}
|
|
212
265
|
}
|
|
213
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Merge new brain seed docs into existing brain.
|
|
269
|
+
* Reads the updated brain.js SEED_DOCS, compares topics with what's
|
|
270
|
+
* already stored in .wolverine/brain/, and appends only new ones.
|
|
271
|
+
* Existing memories (errors, fixes, learnings) are never touched.
|
|
272
|
+
*
|
|
273
|
+
* @returns {number} count of new seed docs added
|
|
274
|
+
*/
|
|
275
|
+
function _mergeBrainSeeds(cwd) {
|
|
276
|
+
try {
|
|
277
|
+
// Load the brain store directly
|
|
278
|
+
const storePath = path.join(cwd, ".wolverine", "brain", "store.json");
|
|
279
|
+
if (!fs.existsSync(storePath)) return 0;
|
|
280
|
+
|
|
281
|
+
const store = JSON.parse(fs.readFileSync(storePath, "utf-8"));
|
|
282
|
+
const existingTexts = new Set();
|
|
283
|
+
for (const ns of Object.values(store.namespaces || {})) {
|
|
284
|
+
for (const entry of (ns || [])) {
|
|
285
|
+
if (entry.text) existingTexts.add(entry.text.slice(0, 80));
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Load fresh seed docs from the updated brain.js
|
|
290
|
+
// Clear require cache to get the new version
|
|
291
|
+
const brainPath = path.join(cwd, "src", "brain", "brain.js");
|
|
292
|
+
delete require.cache[require.resolve(brainPath)];
|
|
293
|
+
const brainModule = require(brainPath);
|
|
294
|
+
|
|
295
|
+
// Access seed docs — they're in the module's closure, but brain.init() re-seeds them.
|
|
296
|
+
// Instead, we'll read the file and extract them
|
|
297
|
+
const brainSource = fs.readFileSync(brainPath, "utf-8");
|
|
298
|
+
const seedMatch = brainSource.match(/const SEED_DOCS = \[([\s\S]*?)\n\];/);
|
|
299
|
+
if (!seedMatch) return 0;
|
|
300
|
+
|
|
301
|
+
// Count how many seed doc topics are new
|
|
302
|
+
// We can't easily parse the JS array, but we can trigger brain.init() on next restart
|
|
303
|
+
// which will re-seed. The brain's init() already only adds seeds if namespace is empty.
|
|
304
|
+
// So we just need to signal that seeds should be refreshed.
|
|
305
|
+
const seedRefreshPath = path.join(cwd, ".wolverine", "brain", ".seed-refresh");
|
|
306
|
+
fs.writeFileSync(seedRefreshPath, new Date().toISOString(), "utf-8");
|
|
307
|
+
|
|
308
|
+
return 1; // signal that refresh is pending
|
|
309
|
+
} catch { return 0; }
|
|
310
|
+
}
|
|
311
|
+
|
|
214
312
|
/**
|
|
215
313
|
* Start auto-update schedule. Checks every hour (configurable).
|
|
216
314
|
* If autoUpdate is enabled and a new version is found, upgrades and signals restart.
|