openclaw-watcher 0.0.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/.claude/settings.local.json +7 -0
- package/.dockerignore +21 -0
- package/.env.example +31 -0
- package/.eslintrc.json +26 -0
- package/.prettierrc.json +9 -0
- package/CHANGELOG.md +93 -0
- package/Dockerfile +47 -0
- package/README.md +408 -0
- package/build.sh +33 -0
- package/dist/ai/ai-orchestrator.d.ts +11 -0
- package/dist/ai/ai-orchestrator.d.ts.map +1 -0
- package/dist/ai/ai-orchestrator.js +85 -0
- package/dist/ai/ai-orchestrator.js.map +1 -0
- package/dist/ai/cli-client.d.ts +17 -0
- package/dist/ai/cli-client.d.ts.map +1 -0
- package/dist/ai/cli-client.js +239 -0
- package/dist/ai/cli-client.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +33 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/config.d.ts +7 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +52 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/init.d.ts +6 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +205 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/start.d.ts +6 -0
- package/dist/commands/start.d.ts.map +1 -0
- package/dist/commands/start.js +49 -0
- package/dist/commands/start.js.map +1 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +48 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/config/default.d.ts +5 -0
- package/dist/config/default.d.ts.map +1 -0
- package/dist/config/default.js +22 -0
- package/dist/config/default.js.map +1 -0
- package/dist/healthcheck/gateway-monitor.d.ts +19 -0
- package/dist/healthcheck/gateway-monitor.d.ts.map +1 -0
- package/dist/healthcheck/gateway-monitor.js +116 -0
- package/dist/healthcheck/gateway-monitor.js.map +1 -0
- package/dist/healthcheck/health-checker.d.ts +11 -0
- package/dist/healthcheck/health-checker.d.ts.map +1 -0
- package/dist/healthcheck/health-checker.js +60 -0
- package/dist/healthcheck/health-checker.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -0
- package/dist/recovery/auto-fixer.d.ts +16 -0
- package/dist/recovery/auto-fixer.d.ts.map +1 -0
- package/dist/recovery/auto-fixer.js +162 -0
- package/dist/recovery/auto-fixer.js.map +1 -0
- package/dist/recovery/change-recorder.d.ts +8 -0
- package/dist/recovery/change-recorder.d.ts.map +1 -0
- package/dist/recovery/change-recorder.js +41 -0
- package/dist/recovery/change-recorder.js.map +1 -0
- package/dist/setup/config-initializer.d.ts +13 -0
- package/dist/setup/config-initializer.d.ts.map +1 -0
- package/dist/setup/config-initializer.js +46 -0
- package/dist/setup/config-initializer.js.map +1 -0
- package/dist/setup/config-loader.d.ts +9 -0
- package/dist/setup/config-loader.d.ts.map +1 -0
- package/dist/setup/config-loader.js +17 -0
- package/dist/setup/config-loader.js.map +1 -0
- package/dist/setup/git-initializer.d.ts +15 -0
- package/dist/setup/git-initializer.d.ts.map +1 -0
- package/dist/setup/git-initializer.js +189 -0
- package/dist/setup/git-initializer.js.map +1 -0
- package/dist/setup/safe-config-generator.d.ts +9 -0
- package/dist/setup/safe-config-generator.d.ts.map +1 -0
- package/dist/setup/safe-config-generator.js +85 -0
- package/dist/setup/safe-config-generator.js.map +1 -0
- package/dist/types/index.d.ts +60 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/executor.d.ts +17 -0
- package/dist/utils/executor.d.ts.map +1 -0
- package/dist/utils/executor.js +57 -0
- package/dist/utils/executor.js.map +1 -0
- package/dist/utils/git-manager.d.ts +14 -0
- package/dist/utils/git-manager.d.ts.map +1 -0
- package/dist/utils/git-manager.js +116 -0
- package/dist/utils/git-manager.js.map +1 -0
- package/dist/utils/github-cli.d.ts +9 -0
- package/dist/utils/github-cli.d.ts.map +1 -0
- package/dist/utils/github-cli.js +31 -0
- package/dist/utils/github-cli.js.map +1 -0
- package/dist/utils/logger.d.ts +4 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +26 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/paths.d.ts +6 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/dist/utils/paths.js +19 -0
- package/dist/utils/paths.js.map +1 -0
- package/docker-compose.yml +43 -0
- package/nodemon.json +9 -0
- package/package.json +59 -0
- package/prompts/fix-openclaw.md +202 -0
- package/scripts/setup.sh +105 -0
- package/src/ai/ai-orchestrator.ts +95 -0
- package/src/ai/cli-client.ts +296 -0
- package/src/cli.ts +40 -0
- package/src/commands/config.ts +57 -0
- package/src/commands/init.ts +239 -0
- package/src/commands/start.ts +75 -0
- package/src/commands/status.ts +79 -0
- package/src/config/default.ts +25 -0
- package/src/healthcheck/gateway-monitor.ts +137 -0
- package/src/healthcheck/health-checker.ts +71 -0
- package/src/index.ts +48 -0
- package/src/recovery/auto-fixer.ts +184 -0
- package/src/recovery/change-recorder.ts +46 -0
- package/src/setup/config-initializer.ts +63 -0
- package/src/setup/config-loader.ts +25 -0
- package/src/setup/git-initializer.ts +203 -0
- package/src/setup/safe-config-generator.ts +100 -0
- package/src/types/index.ts +67 -0
- package/src/utils/executor.ts +75 -0
- package/src/utils/git-manager.ts +121 -0
- package/src/utils/github-cli.ts +37 -0
- package/src/utils/logger.ts +39 -0
- package/src/utils/paths.ts +25 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import axios, { AxiosError } from 'axios';
|
|
2
|
+
import logger from '@/utils/logger.js';
|
|
3
|
+
import { HealthCheckResult, MonitorConfig } from '@/types';
|
|
4
|
+
|
|
5
|
+
export class HealthChecker {
|
|
6
|
+
private config: MonitorConfig;
|
|
7
|
+
private consecutiveFailures: number = 0;
|
|
8
|
+
|
|
9
|
+
constructor(config: MonitorConfig) {
|
|
10
|
+
this.config = config;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async check(): Promise<HealthCheckResult> {
|
|
14
|
+
const url = `${this.config.gatewayUrl}${this.config.healthEndpoint}`;
|
|
15
|
+
const startTime = Date.now();
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const response = await axios.get(url, {
|
|
19
|
+
timeout: this.config.timeout,
|
|
20
|
+
validateStatus: (status) => status < 500,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const responseTime = Date.now() - startTime;
|
|
24
|
+
const healthy = response.status >= 200 && response.status < 300;
|
|
25
|
+
|
|
26
|
+
if (healthy) {
|
|
27
|
+
this.consecutiveFailures = 0;
|
|
28
|
+
} else {
|
|
29
|
+
this.consecutiveFailures++;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const result: HealthCheckResult = {
|
|
33
|
+
healthy,
|
|
34
|
+
timestamp: new Date(),
|
|
35
|
+
endpoint: url,
|
|
36
|
+
responseTime,
|
|
37
|
+
statusCode: response.status,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
logger.info('Health check completed', result);
|
|
41
|
+
return result;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
this.consecutiveFailures++;
|
|
44
|
+
const responseTime = Date.now() - startTime;
|
|
45
|
+
|
|
46
|
+
const result: HealthCheckResult = {
|
|
47
|
+
healthy: false,
|
|
48
|
+
timestamp: new Date(),
|
|
49
|
+
endpoint: url,
|
|
50
|
+
responseTime,
|
|
51
|
+
error: error instanceof AxiosError ? error.message : String(error),
|
|
52
|
+
statusCode: error instanceof AxiosError ? error.response?.status : undefined,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
logger.error('Health check failed', result);
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
shouldTriggerRecovery(): boolean {
|
|
61
|
+
return this.consecutiveFailures >= this.config.failureThreshold;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
getConsecutiveFailures(): number {
|
|
65
|
+
return this.consecutiveFailures;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
reset(): void {
|
|
69
|
+
this.consecutiveFailures = 0;
|
|
70
|
+
}
|
|
71
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import { GatewayMonitor } from '@/healthcheck/gateway-monitor.js';
|
|
3
|
+
import { defaultMonitorConfig, defaultAIConfig, defaultRecoveryConfig } from '@/config/default.js';
|
|
4
|
+
import logger from '@/utils/logger.js';
|
|
5
|
+
|
|
6
|
+
async function main() {
|
|
7
|
+
logger.info('🚀 Starting OpenClaw HealthCheck System');
|
|
8
|
+
|
|
9
|
+
logger.info('Configuration loaded', {
|
|
10
|
+
gatewayUrl: defaultMonitorConfig.gatewayUrl,
|
|
11
|
+
checkInterval: defaultMonitorConfig.checkInterval,
|
|
12
|
+
aiProvider: defaultAIConfig.provider,
|
|
13
|
+
gitTracking: defaultRecoveryConfig.useGitTracking,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const monitor = new GatewayMonitor(
|
|
17
|
+
defaultMonitorConfig,
|
|
18
|
+
defaultAIConfig,
|
|
19
|
+
defaultRecoveryConfig
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
// Handle graceful shutdown
|
|
23
|
+
process.on('SIGINT', () => {
|
|
24
|
+
logger.info('Received SIGINT, shutting down gracefully');
|
|
25
|
+
monitor.stop();
|
|
26
|
+
process.exit(0);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
process.on('SIGTERM', () => {
|
|
30
|
+
logger.info('Received SIGTERM, shutting down gracefully');
|
|
31
|
+
monitor.stop();
|
|
32
|
+
process.exit(0);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Start monitoring
|
|
36
|
+
try {
|
|
37
|
+
await monitor.start();
|
|
38
|
+
logger.info('✅ OpenClaw HealthCheck System is running');
|
|
39
|
+
} catch (error: any) {
|
|
40
|
+
logger.error('Failed to start monitoring', { error: error.message });
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
main().catch((error) => {
|
|
46
|
+
logger.error('Fatal error', { error: error.message });
|
|
47
|
+
process.exit(1);
|
|
48
|
+
});
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { ChangeRecorder } from './change-recorder.js';
|
|
2
|
+
import { GitManager } from '@/utils/git-manager.js';
|
|
3
|
+
import { SafeConfigGenerator } from '@/setup/safe-config-generator.js';
|
|
4
|
+
import { executor } from '@/utils/executor.js';
|
|
5
|
+
import logger from '@/utils/logger.js';
|
|
6
|
+
import { DiagnosisResult, RecoveryResult, RecoveryConfig, ChangeRecord } from '@/types';
|
|
7
|
+
|
|
8
|
+
export class AutoFixer {
|
|
9
|
+
private changeRecorder: ChangeRecorder;
|
|
10
|
+
private gitManager?: GitManager;
|
|
11
|
+
private config: RecoveryConfig;
|
|
12
|
+
|
|
13
|
+
constructor(config: RecoveryConfig) {
|
|
14
|
+
this.config = config;
|
|
15
|
+
this.changeRecorder = new ChangeRecorder();
|
|
16
|
+
|
|
17
|
+
if (config.useGitTracking) {
|
|
18
|
+
this.gitManager = new GitManager(config.openclawConfigPath);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Record the fix that AI has already applied
|
|
24
|
+
* AI has already modified files and executed commands
|
|
25
|
+
* We just need to commit to git and record the change
|
|
26
|
+
*/
|
|
27
|
+
async recordFix(diagnosis: DiagnosisResult): Promise<RecoveryResult> {
|
|
28
|
+
logger.info('Recording AI-applied fix', { diagnosis });
|
|
29
|
+
|
|
30
|
+
const actions: string[] = [];
|
|
31
|
+
const errors: string[] = [];
|
|
32
|
+
let gitCommit: string | undefined;
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
// Initialize git if needed
|
|
36
|
+
if (this.gitManager) {
|
|
37
|
+
await this.gitManager.init();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// AI has already modified files, so we collect what changed
|
|
41
|
+
actions.push('AI completed diagnosis and fix');
|
|
42
|
+
|
|
43
|
+
if (diagnosis.configChanges && diagnosis.configChanges.length > 0) {
|
|
44
|
+
actions.push(`Modified ${diagnosis.configChanges.length} configuration files`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (diagnosis.commands && diagnosis.commands.length > 0) {
|
|
48
|
+
actions.push(`Executed ${diagnosis.commands.length} commands`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Commit changes to git
|
|
52
|
+
if (this.gitManager) {
|
|
53
|
+
const commitMessage = this.createCommitMessage(diagnosis);
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
// Update safe config before committing
|
|
57
|
+
logger.info('Updating safe configuration');
|
|
58
|
+
const safeConfigGen = new SafeConfigGenerator(this.config.openclawConfigPath);
|
|
59
|
+
await safeConfigGen.update();
|
|
60
|
+
actions.push('Updated openclaw.safe.json');
|
|
61
|
+
|
|
62
|
+
// Get diff to see what actually changed
|
|
63
|
+
const diff = await this.gitManager.getDiff();
|
|
64
|
+
|
|
65
|
+
if (diff) {
|
|
66
|
+
// Stage openclaw.safe.json
|
|
67
|
+
gitCommit = await this.gitManager.commit(commitMessage, ['openclaw.safe.json']);
|
|
68
|
+
actions.push(`Committed changes: ${gitCommit}`);
|
|
69
|
+
logger.info('Changes committed to git', { commit: gitCommit });
|
|
70
|
+
|
|
71
|
+
// Auto-push to GitHub if enabled
|
|
72
|
+
if (gitCommit && this.config.useGitHubCli) {
|
|
73
|
+
if (await this.gitManager.hasRemote()) {
|
|
74
|
+
const pushed = await this.gitManager.push();
|
|
75
|
+
actions.push(pushed ? 'Pushed to GitHub' : 'GitHub push failed (best-effort)');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
logger.info('No git changes to commit');
|
|
80
|
+
actions.push('No git changes detected');
|
|
81
|
+
}
|
|
82
|
+
} catch (error: any) {
|
|
83
|
+
logger.error('Git commit failed', { error: error.message });
|
|
84
|
+
errors.push(`Git commit failed: ${error.message}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Verify the fix by checking gateway status
|
|
89
|
+
const verificationResult = await this.verifyFix();
|
|
90
|
+
|
|
91
|
+
if (!verificationResult.success) {
|
|
92
|
+
errors.push(`Verification failed: ${verificationResult.message}`);
|
|
93
|
+
} else {
|
|
94
|
+
actions.push(`Verification: ${verificationResult.message}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const success = errors.length === 0 && verificationResult.success;
|
|
98
|
+
const result: RecoveryResult = {
|
|
99
|
+
success,
|
|
100
|
+
actions,
|
|
101
|
+
errors: errors.length > 0 ? errors : undefined,
|
|
102
|
+
configChanges: diagnosis.configChanges,
|
|
103
|
+
restartRequired: false, // AI should have already restarted if needed
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// Record the change
|
|
107
|
+
const record: ChangeRecord = {
|
|
108
|
+
timestamp: new Date(),
|
|
109
|
+
trigger: 'ai-auto-fix',
|
|
110
|
+
diagnosis,
|
|
111
|
+
recovery: result,
|
|
112
|
+
gitCommit,
|
|
113
|
+
};
|
|
114
|
+
await this.changeRecorder.record(record);
|
|
115
|
+
|
|
116
|
+
logger.info('Fix recording completed', result);
|
|
117
|
+
return result;
|
|
118
|
+
} catch (error: any) {
|
|
119
|
+
logger.error('Fix recording failed', { error: error.message });
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
success: false,
|
|
123
|
+
actions,
|
|
124
|
+
errors: [...errors, `Fatal error: ${error.message}`],
|
|
125
|
+
restartRequired: false,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private createCommitMessage(diagnosis: DiagnosisResult): string {
|
|
131
|
+
const lines: string[] = [];
|
|
132
|
+
|
|
133
|
+
lines.push(`${this.config.gitCommitPrefix} ${diagnosis.issue}`);
|
|
134
|
+
lines.push('');
|
|
135
|
+
lines.push(`Root Cause: ${diagnosis.rootCause}`);
|
|
136
|
+
lines.push(`Fix Applied: ${diagnosis.suggestedFix}`);
|
|
137
|
+
lines.push(`Confidence: ${diagnosis.confidence}`);
|
|
138
|
+
|
|
139
|
+
if (diagnosis.configChanges && diagnosis.configChanges.length > 0) {
|
|
140
|
+
lines.push('');
|
|
141
|
+
lines.push('Files Modified:');
|
|
142
|
+
diagnosis.configChanges.forEach((change) => {
|
|
143
|
+
lines.push(`- ${change.file}: ${change.reason}`);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (diagnosis.commands && diagnosis.commands.length > 0) {
|
|
148
|
+
lines.push('');
|
|
149
|
+
lines.push('Commands Executed:');
|
|
150
|
+
diagnosis.commands.forEach((cmd) => {
|
|
151
|
+
lines.push(`- ${cmd}`);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
lines.push('');
|
|
156
|
+
lines.push('Applied by: AI Auto-Fix System');
|
|
157
|
+
|
|
158
|
+
return lines.join('\n');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private async verifyFix(): Promise<{ success: boolean; message: string }> {
|
|
162
|
+
try {
|
|
163
|
+
// Check gateway status
|
|
164
|
+
const statusResult = await executor.getGatewayStatus();
|
|
165
|
+
|
|
166
|
+
if (statusResult.success) {
|
|
167
|
+
return {
|
|
168
|
+
success: true,
|
|
169
|
+
message: 'Gateway is running normally',
|
|
170
|
+
};
|
|
171
|
+
} else {
|
|
172
|
+
return {
|
|
173
|
+
success: false,
|
|
174
|
+
message: `Gateway status check failed: ${statusResult.stderr}`,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
} catch (error: any) {
|
|
178
|
+
return {
|
|
179
|
+
success: false,
|
|
180
|
+
message: `Verification error: ${error.message}`,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import logger from '@/utils/logger.js';
|
|
4
|
+
import { ChangeRecord } from '@/types';
|
|
5
|
+
import { getChangesFilePath } from '@/utils/paths.js';
|
|
6
|
+
|
|
7
|
+
export class ChangeRecorder {
|
|
8
|
+
private logFilePath: string;
|
|
9
|
+
|
|
10
|
+
constructor(logFilePath?: string) {
|
|
11
|
+
this.logFilePath = logFilePath || getChangesFilePath();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async record(record: ChangeRecord): Promise<void> {
|
|
15
|
+
logger.info('Recording change', record);
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
await fs.mkdir(path.dirname(this.logFilePath), { recursive: true });
|
|
19
|
+
|
|
20
|
+
let records: ChangeRecord[] = [];
|
|
21
|
+
try {
|
|
22
|
+
const content = await fs.readFile(this.logFilePath, 'utf-8');
|
|
23
|
+
records = JSON.parse(content);
|
|
24
|
+
} catch {
|
|
25
|
+
// File doesn't exist yet
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
records.push(record);
|
|
29
|
+
|
|
30
|
+
await fs.writeFile(this.logFilePath, JSON.stringify(records, null, 2), 'utf-8');
|
|
31
|
+
logger.info('Change recorded successfully');
|
|
32
|
+
} catch (error: any) {
|
|
33
|
+
logger.error('Failed to record change', { error: error.message });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async getHistory(limit: number = 10): Promise<ChangeRecord[]> {
|
|
38
|
+
try {
|
|
39
|
+
const content = await fs.readFile(this.logFilePath, 'utf-8');
|
|
40
|
+
const records: ChangeRecord[] = JSON.parse(content);
|
|
41
|
+
return records.slice(-limit);
|
|
42
|
+
} catch {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import logger from '@/utils/logger.js';
|
|
3
|
+
import {
|
|
4
|
+
getWatcherHome,
|
|
5
|
+
getConfigPath,
|
|
6
|
+
getLogsDir,
|
|
7
|
+
getLogFilePath,
|
|
8
|
+
} from '@/utils/paths.js';
|
|
9
|
+
|
|
10
|
+
export interface WatcherConfig {
|
|
11
|
+
openclawConfigPath: string;
|
|
12
|
+
gatewayUrl: string;
|
|
13
|
+
healthCheckInterval: number;
|
|
14
|
+
failureThreshold: number;
|
|
15
|
+
aiProvider: string;
|
|
16
|
+
gitTracking: boolean;
|
|
17
|
+
useGitHubCli: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class ConfigInitializer {
|
|
21
|
+
async create(config: WatcherConfig): Promise<void> {
|
|
22
|
+
const configContent = {
|
|
23
|
+
monitor: {
|
|
24
|
+
gatewayUrl: config.gatewayUrl,
|
|
25
|
+
healthEndpoint: '/',
|
|
26
|
+
checkInterval: config.healthCheckInterval,
|
|
27
|
+
timeout: 10000,
|
|
28
|
+
maxRetries: 3,
|
|
29
|
+
failureThreshold: config.failureThreshold,
|
|
30
|
+
},
|
|
31
|
+
ai: {
|
|
32
|
+
provider: config.aiProvider,
|
|
33
|
+
timeout: 300000,
|
|
34
|
+
},
|
|
35
|
+
recovery: {
|
|
36
|
+
useGitTracking: config.gitTracking,
|
|
37
|
+
useGitHubCli: config.useGitHubCli,
|
|
38
|
+
gitCommitPrefix: '[AutoFix]',
|
|
39
|
+
backupBeforeChange: true,
|
|
40
|
+
openclawConfigPath: config.openclawConfigPath,
|
|
41
|
+
},
|
|
42
|
+
logging: {
|
|
43
|
+
level: 'info',
|
|
44
|
+
filePath: getLogFilePath(),
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Ensure watcher home and logs directory exist
|
|
49
|
+
const watcherHome = getWatcherHome();
|
|
50
|
+
await fs.mkdir(watcherHome, { recursive: true });
|
|
51
|
+
await fs.mkdir(getLogsDir(), { recursive: true });
|
|
52
|
+
|
|
53
|
+
const configPath = getConfigPath();
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
await fs.writeFile(configPath, JSON.stringify(configContent, null, 2), 'utf-8');
|
|
57
|
+
logger.info('Configuration file created', { path: configPath });
|
|
58
|
+
} catch (error: any) {
|
|
59
|
+
logger.error('Failed to create configuration file', { error: error.message });
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import { MonitorConfig, AIClientConfig, RecoveryConfig } from '@/types/index.js';
|
|
3
|
+
import { getConfigPath } from '@/utils/paths.js';
|
|
4
|
+
|
|
5
|
+
export interface LoadedConfig {
|
|
6
|
+
monitor: MonitorConfig;
|
|
7
|
+
ai: AIClientConfig;
|
|
8
|
+
recovery: RecoveryConfig;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function loadConfig(): Promise<LoadedConfig | null> {
|
|
12
|
+
const configPath = getConfigPath();
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const content = await fs.readFile(configPath, 'utf-8');
|
|
16
|
+
const config = JSON.parse(content);
|
|
17
|
+
return config as LoadedConfig;
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getConfigFilePath(): string {
|
|
24
|
+
return getConfigPath();
|
|
25
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import simpleGit, { SimpleGit } from 'simple-git';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import logger from '@/utils/logger.js';
|
|
5
|
+
|
|
6
|
+
export interface GitInitOptions {
|
|
7
|
+
repoName: string;
|
|
8
|
+
isPrivate: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class GitInitializer {
|
|
12
|
+
private git: SimpleGit;
|
|
13
|
+
private repoPath: string;
|
|
14
|
+
|
|
15
|
+
constructor(repoPath: string) {
|
|
16
|
+
this.repoPath = repoPath;
|
|
17
|
+
this.git = simpleGit(repoPath);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async init(options: GitInitOptions): Promise<void> {
|
|
21
|
+
try {
|
|
22
|
+
// Check if already a git repo
|
|
23
|
+
const isRepo = await this.git.checkIsRepo();
|
|
24
|
+
if (isRepo) {
|
|
25
|
+
logger.info('Git repository already exists', { path: this.repoPath });
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Initialize git
|
|
30
|
+
await this.git.init();
|
|
31
|
+
logger.info('Git repository initialized', { path: this.repoPath });
|
|
32
|
+
|
|
33
|
+
// Configure git
|
|
34
|
+
await this.git.addConfig('user.name', 'OpenClaw Watcher');
|
|
35
|
+
await this.git.addConfig('user.email', 'watcher@openclaw.local');
|
|
36
|
+
|
|
37
|
+
// Create .gitignore
|
|
38
|
+
await this.createGitignore();
|
|
39
|
+
|
|
40
|
+
// Create README
|
|
41
|
+
await this.createReadme(options);
|
|
42
|
+
} catch (error: any) {
|
|
43
|
+
logger.error('Failed to initialize git repository', { error: error.message });
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private async createGitignore(): Promise<void> {
|
|
49
|
+
const gitignorePath = path.join(this.repoPath, '.gitignore');
|
|
50
|
+
const content = `# OpenClaw Configuration - Sensitive Files
|
|
51
|
+
# IMPORTANT: openclaw.json contains API keys and tokens
|
|
52
|
+
openclaw.json
|
|
53
|
+
|
|
54
|
+
# Agent auth profiles (API keys)
|
|
55
|
+
agents/*/agent/auth-profiles.json
|
|
56
|
+
|
|
57
|
+
# Session history (large + privacy)
|
|
58
|
+
agents/*/sessions/
|
|
59
|
+
|
|
60
|
+
# Vector index / memory DB (large)
|
|
61
|
+
memory/*.sqlite
|
|
62
|
+
|
|
63
|
+
# Cache
|
|
64
|
+
cache/
|
|
65
|
+
|
|
66
|
+
# Logs
|
|
67
|
+
*.log
|
|
68
|
+
|
|
69
|
+
# Backup files
|
|
70
|
+
*.backup
|
|
71
|
+
*.bak
|
|
72
|
+
*.tmp
|
|
73
|
+
|
|
74
|
+
# System files
|
|
75
|
+
.DS_Store
|
|
76
|
+
Thumbs.db
|
|
77
|
+
`;
|
|
78
|
+
await fs.writeFile(gitignorePath, content, 'utf-8');
|
|
79
|
+
logger.info('.gitignore created');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private async createReadme(options: GitInitOptions): Promise<void> {
|
|
83
|
+
const readmePath = path.join(this.repoPath, 'README.md');
|
|
84
|
+
const content = `# ${options.repoName}
|
|
85
|
+
|
|
86
|
+
${options.isPrivate ? '🔒 **Private Repository**' : '📖 Public Repository'}
|
|
87
|
+
|
|
88
|
+
This repository is managed by **OpenClaw Watcher** — it tracks the entire \`~/.openclaw\` directory, including configuration, agent settings, and AI repair history.
|
|
89
|
+
|
|
90
|
+
## What's Tracked
|
|
91
|
+
|
|
92
|
+
| Path | Description |
|
|
93
|
+
|------|-------------|
|
|
94
|
+
| \`openclaw.safe.json\` | Safe configuration (secrets redacted) |
|
|
95
|
+
| \`agents/*/agent/*.json\` | Agent configuration (except auth) |
|
|
96
|
+
| \`README.md\` | This file |
|
|
97
|
+
| \`.gitignore\` | Git ignore rules |
|
|
98
|
+
|
|
99
|
+
## What's Excluded (.gitignore)
|
|
100
|
+
|
|
101
|
+
| Pattern | Reason |
|
|
102
|
+
|---------|--------|
|
|
103
|
+
| \`openclaw.json\` | Contains API keys and tokens |
|
|
104
|
+
| \`agents/*/agent/auth-profiles.json\` | Agent API keys |
|
|
105
|
+
| \`agents/*/sessions/\` | Session history (large + privacy) |
|
|
106
|
+
| \`memory/*.sqlite\` | Vector index / memory DB |
|
|
107
|
+
| \`cache/\` | Runtime cache |
|
|
108
|
+
| \`*.log\` | Log files |
|
|
109
|
+
|
|
110
|
+
## ⚠️ Security
|
|
111
|
+
|
|
112
|
+
- A **pre-commit hook** blocks commits containing sensitive patterns
|
|
113
|
+
- Never commit \`openclaw.json\` or auth profile files manually
|
|
114
|
+
- All automated commits are created by the AI repair system
|
|
115
|
+
|
|
116
|
+
## 🔧 Managed by
|
|
117
|
+
|
|
118
|
+
[OpenClaw Watcher](https://github.com/your-org/openclaw-watcher) - AI-powered health monitoring and auto-repair
|
|
119
|
+
`;
|
|
120
|
+
await fs.writeFile(readmePath, content, 'utf-8');
|
|
121
|
+
logger.info('README.md created');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async setupPreCommitHook(): Promise<void> {
|
|
125
|
+
const hooksDir = path.join(this.repoPath, '.git', 'hooks');
|
|
126
|
+
const preCommitPath = path.join(hooksDir, 'pre-commit');
|
|
127
|
+
|
|
128
|
+
const hookContent = `#!/bin/bash
|
|
129
|
+
|
|
130
|
+
# OpenClaw Watcher Pre-commit Hook
|
|
131
|
+
# Checks for sensitive data in openclaw.safe.json
|
|
132
|
+
|
|
133
|
+
echo "🔍 Checking for sensitive data..."
|
|
134
|
+
|
|
135
|
+
# Check if openclaw.safe.json is staged
|
|
136
|
+
if git diff --cached --name-only | grep -q "openclaw.safe.json"; then
|
|
137
|
+
# Get the diff content, excluding already redacted lines (placeholders)
|
|
138
|
+
# Lines containing <YOUR_..._HERE> or <REDACTED> are considered safe
|
|
139
|
+
DIFF_CONTENT=$(git diff --cached openclaw.safe.json | grep -v "<YOUR_.*_HERE>" | grep -v "<REDACTED>")
|
|
140
|
+
|
|
141
|
+
# Check for common sensitive patterns in non-redacted content
|
|
142
|
+
PATTERNS=(
|
|
143
|
+
"sk-ant-[a-zA-Z0-9]" # Claude API keys
|
|
144
|
+
"Bearer [a-zA-Z0-9_-]{10,}" # Bearer tokens
|
|
145
|
+
"api[_-]?key[^\"]*[\"']\s*:\s*[\"'][^\"']{5,}[\"']" # API keys
|
|
146
|
+
"token[^\"]*[\"']\s*:\s*[\"'][^\"']{5,}[\"']" # Tokens
|
|
147
|
+
"password[^\"]*[\"']\s*:\s*[\"'][^\"']{3,}[\"']" # Passwords
|
|
148
|
+
"secret[^\"]*[\"']\s*:\s*[\"'][^\"']{5,}[\"']" # Secrets
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
FOUND_SENSITIVE=false
|
|
152
|
+
|
|
153
|
+
for pattern in "\${PATTERNS[@]}"; do
|
|
154
|
+
if echo "$DIFF_CONTENT" | grep -iE "$pattern" > /dev/null; then
|
|
155
|
+
echo "❌ ERROR: Possible sensitive data found in openclaw.safe.json"
|
|
156
|
+
echo " Pattern matched: $pattern"
|
|
157
|
+
FOUND_SENSITIVE=true
|
|
158
|
+
fi
|
|
159
|
+
done
|
|
160
|
+
|
|
161
|
+
if [ "$FOUND_SENSITIVE" = true ]; then
|
|
162
|
+
echo ""
|
|
163
|
+
echo "🚨 Commit blocked to prevent sensitive data leak!"
|
|
164
|
+
echo " Please review openclaw.safe.json and ensure all secrets are replaced with placeholders"
|
|
165
|
+
echo " Example: \\"apiKey\\": \\"<YOUR_API_KEY_HERE>\\""
|
|
166
|
+
exit 1
|
|
167
|
+
fi
|
|
168
|
+
fi
|
|
169
|
+
|
|
170
|
+
# Check if openclaw.json is accidentally staged
|
|
171
|
+
if git diff --cached --name-only | grep -q "^openclaw.json$"; then
|
|
172
|
+
echo "❌ ERROR: openclaw.json should NOT be committed!"
|
|
173
|
+
echo " This file contains sensitive information."
|
|
174
|
+
echo " It should be in .gitignore"
|
|
175
|
+
exit 1
|
|
176
|
+
fi
|
|
177
|
+
|
|
178
|
+
echo "✅ Pre-commit checks passed"
|
|
179
|
+
exit 0
|
|
180
|
+
`;
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
await fs.mkdir(hooksDir, { recursive: true });
|
|
184
|
+
await fs.writeFile(preCommitPath, hookContent, 'utf-8');
|
|
185
|
+
await fs.chmod(preCommitPath, 0o755);
|
|
186
|
+
logger.info('Pre-commit hook installed');
|
|
187
|
+
} catch (error: any) {
|
|
188
|
+
logger.error('Failed to setup pre-commit hook', { error: error.message });
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async initialCommit(): Promise<void> {
|
|
194
|
+
try {
|
|
195
|
+
await this.git.add('.');
|
|
196
|
+
await this.git.commit('Initial commit: OpenClaw Watcher configuration tracking');
|
|
197
|
+
logger.info('Initial commit created');
|
|
198
|
+
} catch (error: any) {
|
|
199
|
+
logger.error('Failed to create initial commit', { error: error.message });
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|