@robbiesrobotics/alice-agents 1.1.1 → 1.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/SELF-HEALING-SPEC.md +2503 -0
- package/lib/installer.mjs +17 -1
- package/package.json +4 -1
- package/snapshots/schema-snapshot.json +221 -0
- package/snapshots/tool-snapshot.json +37 -0
- package/tools/compatibility-checker.mjs +268 -0
- package/tools/local-remediation.mjs +231 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* A.L.I.C.E. Local Remediation
|
|
4
|
+
* Runs on user's machine via OpenClaw cron.
|
|
5
|
+
* Detects OpenClaw version changes and auto-fixes compatible issues.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync, writeFileSync, existsSync, copyFileSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { execSync } from 'node:child_process';
|
|
11
|
+
|
|
12
|
+
const HOME = process.env.HOME;
|
|
13
|
+
const OPENCLAW_CONFIG = join(HOME, '.openclaw', 'openclaw.json');
|
|
14
|
+
const ALICE_MANIFEST = join(HOME, '.openclaw', '.alice-manifest.json');
|
|
15
|
+
const ALERT_FILE = join(HOME, '.openclaw', '.alice-health-alert.json');
|
|
16
|
+
const verbose = process.argv.includes('--verbose');
|
|
17
|
+
|
|
18
|
+
function log(...a) { if (verbose) console.log('[alice-remediation]', ...a); }
|
|
19
|
+
function warn(msg) { console.warn(`⚠️ ${msg}`); }
|
|
20
|
+
function info(msg) { console.log(`ℹ️ ${msg}`); }
|
|
21
|
+
|
|
22
|
+
function getOpenClawVersion() {
|
|
23
|
+
try {
|
|
24
|
+
const out = execSync('openclaw --version 2>/dev/null', { encoding: 'utf8', stdio: ['pipe','pipe','pipe'] });
|
|
25
|
+
const m = out.match(/(\d{4}\.\d+\.\d+)/);
|
|
26
|
+
return m ? m[1] : null;
|
|
27
|
+
} catch { return null; }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function loadJSON(path) {
|
|
31
|
+
if (!existsSync(path)) return null;
|
|
32
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function saveJSON(path, obj) {
|
|
36
|
+
writeFileSync(path, JSON.stringify(obj, null, 2));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function backupConfig() {
|
|
40
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
|
|
41
|
+
const backup = `${OPENCLAW_CONFIG}.bak.selfheal-${timestamp}`;
|
|
42
|
+
copyFileSync(OPENCLAW_CONFIG, backup);
|
|
43
|
+
log(`Config backed up to: ${backup}`);
|
|
44
|
+
return backup;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function rollback(backup) {
|
|
48
|
+
copyFileSync(backup, OPENCLAW_CONFIG);
|
|
49
|
+
warn(`Rolled back config from: ${backup}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function verifyConfig() {
|
|
53
|
+
try {
|
|
54
|
+
execSync('openclaw status --format=json 2>/dev/null', { encoding: 'utf8', stdio: ['pipe','pipe','pipe'] });
|
|
55
|
+
return true;
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function applyPatch(config, patch) {
|
|
62
|
+
const { type, path: fieldPath, from, to, value } = patch;
|
|
63
|
+
|
|
64
|
+
function getNestedValue(obj, path) {
|
|
65
|
+
return path.split('.').reduce((o, k) => o?.[k], obj);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function setNestedValue(obj, path, val) {
|
|
69
|
+
const keys = path.split('.');
|
|
70
|
+
let current = obj;
|
|
71
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
72
|
+
if (!current[keys[i]]) current[keys[i]] = {};
|
|
73
|
+
current = current[keys[i]];
|
|
74
|
+
}
|
|
75
|
+
current[keys[keys.length - 1]] = val;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (type === 'value-rename') {
|
|
79
|
+
// Find and rename all occurrences of `from` to `to` in string values
|
|
80
|
+
JSON.stringify(config)
|
|
81
|
+
.split('"' + from + '"')
|
|
82
|
+
.join('"' + to + '"');
|
|
83
|
+
log(` Renamed: "${from}" → "${to}"`);
|
|
84
|
+
} else if (type === 'field-rename') {
|
|
85
|
+
const oldVal = getNestedValue(config, from);
|
|
86
|
+
if (oldVal !== undefined) {
|
|
87
|
+
setNestedValue(config, to, oldVal);
|
|
88
|
+
// Delete old field
|
|
89
|
+
const oldKeys = from.split('.');
|
|
90
|
+
let current = config;
|
|
91
|
+
for (let i = 0; i < oldKeys.length - 1; i++) {
|
|
92
|
+
current = current[oldKeys[i]];
|
|
93
|
+
}
|
|
94
|
+
delete current[oldKeys[oldKeys.length - 1]];
|
|
95
|
+
log(` Renamed field: ${from} → ${to}`);
|
|
96
|
+
}
|
|
97
|
+
} else if (type === 'field-add') {
|
|
98
|
+
const current = getNestedValue(config, fieldPath.split('.').slice(0, -1).join('.'));
|
|
99
|
+
if (current) {
|
|
100
|
+
const key = fieldPath.split('.').pop();
|
|
101
|
+
current[key] = value;
|
|
102
|
+
log(` Added field: ${fieldPath} = ${JSON.stringify(value)}`);
|
|
103
|
+
}
|
|
104
|
+
} else if (type === 'tool-rename') {
|
|
105
|
+
let changed = 0;
|
|
106
|
+
for (const agent of config?.agents?.list || []) {
|
|
107
|
+
const tools = agent?.tools || {};
|
|
108
|
+
for (const list of ['allow', 'alsoAllow']) {
|
|
109
|
+
if (tools[list]) {
|
|
110
|
+
const idx = tools[list].indexOf(from);
|
|
111
|
+
if (idx !== -1) {
|
|
112
|
+
tools[list][idx] = to;
|
|
113
|
+
changed++;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (changed > 0) log(` Renamed tool: "${from}" → "${to}" (${changed} references)`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return config;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function main() {
|
|
125
|
+
const currentVersion = getOpenClawVersion();
|
|
126
|
+
if (!currentVersion) {
|
|
127
|
+
warn('Could not detect OpenClaw version');
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const manifest = loadJSON(ALICE_MANIFEST) || {
|
|
132
|
+
installedVersion: 'unknown',
|
|
133
|
+
installedAt: new Date().toISOString(),
|
|
134
|
+
compatibilityVersion: 'unknown'
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const prevVersion = manifest.compatibilityVersion || 'unknown';
|
|
138
|
+
|
|
139
|
+
log(`OpenClaw version: ${currentVersion}`);
|
|
140
|
+
log(`Previous checked: ${prevVersion}`);
|
|
141
|
+
|
|
142
|
+
if (currentVersion === prevVersion) {
|
|
143
|
+
log('No version change detected — nothing to do');
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
log(`Version mismatch — checking for breaking changes...`);
|
|
148
|
+
|
|
149
|
+
// Try to fetch or run compatibility report
|
|
150
|
+
let report = null;
|
|
151
|
+
const reportPath = join(HOME, '.openclaw', `.alice-compat-${currentVersion}.json`);
|
|
152
|
+
|
|
153
|
+
// Try to use the installed compatibility checker
|
|
154
|
+
try {
|
|
155
|
+
const checkerPath = require.resolve('@robbiesrobotics/alice-agents/tools/compatibility-checker.mjs');
|
|
156
|
+
execSync(`node ${checkerPath} --output=${reportPath}`, { stdio: 'pipe' });
|
|
157
|
+
report = loadJSON(reportPath);
|
|
158
|
+
} catch {
|
|
159
|
+
log('Could not run compatibility checker');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!report) {
|
|
163
|
+
warn('No compatibility report available');
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const { breakingChanges = [] } = report;
|
|
168
|
+
const autoFixable = breakingChanges.filter(c => c.autoFixable);
|
|
169
|
+
const needsReview = breakingChanges.filter(c => !c.autoFixable);
|
|
170
|
+
|
|
171
|
+
log(`Found: ${autoFixable.length} auto-fixable, ${needsReview.length} need review`);
|
|
172
|
+
|
|
173
|
+
// Auto-fix if any
|
|
174
|
+
if (autoFixable.length > 0) {
|
|
175
|
+
const config = loadJSON(OPENCLAW_CONFIG);
|
|
176
|
+
const backup = backupConfig();
|
|
177
|
+
|
|
178
|
+
let fixed = 0;
|
|
179
|
+
for (const change of autoFixable) {
|
|
180
|
+
try {
|
|
181
|
+
applyPatch(config, change.fix);
|
|
182
|
+
fixed++;
|
|
183
|
+
} catch (err) {
|
|
184
|
+
warn(`Failed to apply patch for ${change.field}: ${err.message}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (fixed === autoFixable.length) {
|
|
189
|
+
saveJSON(OPENCLAW_CONFIG, config);
|
|
190
|
+
if (!verifyConfig()) {
|
|
191
|
+
rollback(backup);
|
|
192
|
+
warn('Config verification failed — rolled back');
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
info(`Auto-patched ${fixed} issue(s) for OpenClaw ${currentVersion}`);
|
|
196
|
+
} else {
|
|
197
|
+
warn(`Only patched ${fixed}/${autoFixable.length} — rolling back`);
|
|
198
|
+
rollback(backup);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Write escalation alerts if needed
|
|
204
|
+
if (needsReview.length > 0) {
|
|
205
|
+
const alerts = {
|
|
206
|
+
timestamp: new Date().toISOString(),
|
|
207
|
+
openclawVersion: currentVersion,
|
|
208
|
+
previousVersion: prevVersion,
|
|
209
|
+
alerts: needsReview.map(c => ({
|
|
210
|
+
category: c.category,
|
|
211
|
+
severity: c.severity,
|
|
212
|
+
field: c.field,
|
|
213
|
+
description: c.change,
|
|
214
|
+
actionRequired: true
|
|
215
|
+
}))
|
|
216
|
+
};
|
|
217
|
+
saveJSON(ALERT_FILE, alerts);
|
|
218
|
+
warn(`${needsReview.length} issue(s) need review — see Mission Control > Compatibility`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Update manifest
|
|
222
|
+
manifest.compatibilityVersion = currentVersion;
|
|
223
|
+
manifest.lastCheckedAt = new Date().toISOString();
|
|
224
|
+
saveJSON(ALICE_MANIFEST, manifest);
|
|
225
|
+
|
|
226
|
+
log('Done');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
main().catch(err => {
|
|
230
|
+
warn(`Remediation error: ${err.message}`);
|
|
231
|
+
});
|