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.
Files changed (130) hide show
  1. package/.claude/settings.local.json +7 -0
  2. package/.dockerignore +21 -0
  3. package/.env.example +31 -0
  4. package/.eslintrc.json +26 -0
  5. package/.prettierrc.json +9 -0
  6. package/CHANGELOG.md +93 -0
  7. package/Dockerfile +47 -0
  8. package/README.md +408 -0
  9. package/build.sh +33 -0
  10. package/dist/ai/ai-orchestrator.d.ts +11 -0
  11. package/dist/ai/ai-orchestrator.d.ts.map +1 -0
  12. package/dist/ai/ai-orchestrator.js +85 -0
  13. package/dist/ai/ai-orchestrator.js.map +1 -0
  14. package/dist/ai/cli-client.d.ts +17 -0
  15. package/dist/ai/cli-client.d.ts.map +1 -0
  16. package/dist/ai/cli-client.js +239 -0
  17. package/dist/ai/cli-client.js.map +1 -0
  18. package/dist/cli.d.ts +3 -0
  19. package/dist/cli.d.ts.map +1 -0
  20. package/dist/cli.js +33 -0
  21. package/dist/cli.js.map +1 -0
  22. package/dist/commands/config.d.ts +7 -0
  23. package/dist/commands/config.d.ts.map +1 -0
  24. package/dist/commands/config.js +52 -0
  25. package/dist/commands/config.js.map +1 -0
  26. package/dist/commands/init.d.ts +6 -0
  27. package/dist/commands/init.d.ts.map +1 -0
  28. package/dist/commands/init.js +205 -0
  29. package/dist/commands/init.js.map +1 -0
  30. package/dist/commands/start.d.ts +6 -0
  31. package/dist/commands/start.d.ts.map +1 -0
  32. package/dist/commands/start.js +49 -0
  33. package/dist/commands/start.js.map +1 -0
  34. package/dist/commands/status.d.ts +2 -0
  35. package/dist/commands/status.d.ts.map +1 -0
  36. package/dist/commands/status.js +48 -0
  37. package/dist/commands/status.js.map +1 -0
  38. package/dist/config/default.d.ts +5 -0
  39. package/dist/config/default.d.ts.map +1 -0
  40. package/dist/config/default.js +22 -0
  41. package/dist/config/default.js.map +1 -0
  42. package/dist/healthcheck/gateway-monitor.d.ts +19 -0
  43. package/dist/healthcheck/gateway-monitor.d.ts.map +1 -0
  44. package/dist/healthcheck/gateway-monitor.js +116 -0
  45. package/dist/healthcheck/gateway-monitor.js.map +1 -0
  46. package/dist/healthcheck/health-checker.d.ts +11 -0
  47. package/dist/healthcheck/health-checker.d.ts.map +1 -0
  48. package/dist/healthcheck/health-checker.js +60 -0
  49. package/dist/healthcheck/health-checker.js.map +1 -0
  50. package/dist/index.d.ts +2 -0
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.js +39 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/recovery/auto-fixer.d.ts +16 -0
  55. package/dist/recovery/auto-fixer.d.ts.map +1 -0
  56. package/dist/recovery/auto-fixer.js +162 -0
  57. package/dist/recovery/auto-fixer.js.map +1 -0
  58. package/dist/recovery/change-recorder.d.ts +8 -0
  59. package/dist/recovery/change-recorder.d.ts.map +1 -0
  60. package/dist/recovery/change-recorder.js +41 -0
  61. package/dist/recovery/change-recorder.js.map +1 -0
  62. package/dist/setup/config-initializer.d.ts +13 -0
  63. package/dist/setup/config-initializer.d.ts.map +1 -0
  64. package/dist/setup/config-initializer.js +46 -0
  65. package/dist/setup/config-initializer.js.map +1 -0
  66. package/dist/setup/config-loader.d.ts +9 -0
  67. package/dist/setup/config-loader.d.ts.map +1 -0
  68. package/dist/setup/config-loader.js +17 -0
  69. package/dist/setup/config-loader.js.map +1 -0
  70. package/dist/setup/git-initializer.d.ts +15 -0
  71. package/dist/setup/git-initializer.d.ts.map +1 -0
  72. package/dist/setup/git-initializer.js +189 -0
  73. package/dist/setup/git-initializer.js.map +1 -0
  74. package/dist/setup/safe-config-generator.d.ts +9 -0
  75. package/dist/setup/safe-config-generator.d.ts.map +1 -0
  76. package/dist/setup/safe-config-generator.js +85 -0
  77. package/dist/setup/safe-config-generator.js.map +1 -0
  78. package/dist/types/index.d.ts +60 -0
  79. package/dist/types/index.d.ts.map +1 -0
  80. package/dist/types/index.js +2 -0
  81. package/dist/types/index.js.map +1 -0
  82. package/dist/utils/executor.d.ts +17 -0
  83. package/dist/utils/executor.d.ts.map +1 -0
  84. package/dist/utils/executor.js +57 -0
  85. package/dist/utils/executor.js.map +1 -0
  86. package/dist/utils/git-manager.d.ts +14 -0
  87. package/dist/utils/git-manager.d.ts.map +1 -0
  88. package/dist/utils/git-manager.js +116 -0
  89. package/dist/utils/git-manager.js.map +1 -0
  90. package/dist/utils/github-cli.d.ts +9 -0
  91. package/dist/utils/github-cli.d.ts.map +1 -0
  92. package/dist/utils/github-cli.js +31 -0
  93. package/dist/utils/github-cli.js.map +1 -0
  94. package/dist/utils/logger.d.ts +4 -0
  95. package/dist/utils/logger.d.ts.map +1 -0
  96. package/dist/utils/logger.js +26 -0
  97. package/dist/utils/logger.js.map +1 -0
  98. package/dist/utils/paths.d.ts +6 -0
  99. package/dist/utils/paths.d.ts.map +1 -0
  100. package/dist/utils/paths.js +19 -0
  101. package/dist/utils/paths.js.map +1 -0
  102. package/docker-compose.yml +43 -0
  103. package/nodemon.json +9 -0
  104. package/package.json +59 -0
  105. package/prompts/fix-openclaw.md +202 -0
  106. package/scripts/setup.sh +105 -0
  107. package/src/ai/ai-orchestrator.ts +95 -0
  108. package/src/ai/cli-client.ts +296 -0
  109. package/src/cli.ts +40 -0
  110. package/src/commands/config.ts +57 -0
  111. package/src/commands/init.ts +239 -0
  112. package/src/commands/start.ts +75 -0
  113. package/src/commands/status.ts +79 -0
  114. package/src/config/default.ts +25 -0
  115. package/src/healthcheck/gateway-monitor.ts +137 -0
  116. package/src/healthcheck/health-checker.ts +71 -0
  117. package/src/index.ts +48 -0
  118. package/src/recovery/auto-fixer.ts +184 -0
  119. package/src/recovery/change-recorder.ts +46 -0
  120. package/src/setup/config-initializer.ts +63 -0
  121. package/src/setup/config-loader.ts +25 -0
  122. package/src/setup/git-initializer.ts +203 -0
  123. package/src/setup/safe-config-generator.ts +100 -0
  124. package/src/types/index.ts +67 -0
  125. package/src/utils/executor.ts +75 -0
  126. package/src/utils/git-manager.ts +121 -0
  127. package/src/utils/github-cli.ts +37 -0
  128. package/src/utils/logger.ts +39 -0
  129. package/src/utils/paths.ts +25 -0
  130. package/tsconfig.json +29 -0
package/src/cli.ts ADDED
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { initCommand } from './commands/init.js';
5
+ import { startCommand } from './commands/start.js';
6
+ import { statusCommand } from './commands/status.js';
7
+ import { configCommand } from './commands/config.js';
8
+
9
+ const program = new Command();
10
+
11
+ program
12
+ .name('openclaw-watcher')
13
+ .description('AI-powered automated health monitoring and self-healing for OpenClaw Gateway')
14
+ .version('2.0.0');
15
+
16
+ program
17
+ .command('init')
18
+ .description('Initialize OpenClaw Watcher with interactive configuration')
19
+ .option('--no-git', 'Skip Git repository initialization')
20
+ .action(initCommand);
21
+
22
+ program
23
+ .command('start')
24
+ .description('Start health monitoring and auto-repair service')
25
+ .option('-d, --daemon', 'Run as daemon process')
26
+ .action(startCommand);
27
+
28
+ program
29
+ .command('status')
30
+ .description('Show current monitoring status')
31
+ .action(statusCommand);
32
+
33
+ program
34
+ .command('config')
35
+ .description('Manage configuration')
36
+ .option('-s, --show', 'Show current configuration')
37
+ .option('-e, --edit', 'Edit configuration file')
38
+ .action(configCommand);
39
+
40
+ program.parse();
@@ -0,0 +1,57 @@
1
+ import chalk from 'chalk';
2
+ import { exec } from 'child_process';
3
+ import { promisify } from 'util';
4
+ import { loadConfig, getConfigFilePath } from '@/setup/config-loader.js';
5
+ import fs from 'fs/promises';
6
+
7
+ const execAsync = promisify(exec);
8
+
9
+ interface ConfigOptions {
10
+ show?: boolean;
11
+ edit?: boolean;
12
+ }
13
+
14
+ export async function configCommand(options: ConfigOptions) {
15
+ const configPath = getConfigFilePath();
16
+
17
+ if (options.show) {
18
+ try {
19
+ const config = await loadConfig();
20
+ if (!config) {
21
+ console.log(chalk.yellow('Configuration not found'));
22
+ return;
23
+ }
24
+
25
+ console.log(chalk.bold.cyan('\n📋 Current Configuration\n'));
26
+ console.log(JSON.stringify(config, null, 2));
27
+ console.log('');
28
+ } catch (error: any) {
29
+ console.error(chalk.red(`Error: ${error.message}`));
30
+ process.exit(1);
31
+ }
32
+ } else if (options.edit) {
33
+ try {
34
+ const editor = process.env.EDITOR || 'vim';
35
+ await execAsync(`${editor} ${configPath}`);
36
+ console.log(chalk.green('✓ Configuration updated'));
37
+ } catch (error: any) {
38
+ console.error(chalk.red(`Error: ${error.message}`));
39
+ process.exit(1);
40
+ }
41
+ } else {
42
+ // Show config file path
43
+ try {
44
+ await fs.access(configPath);
45
+ console.log(chalk.cyan('Configuration file:'), chalk.white(configPath));
46
+ console.log('');
47
+ console.log(chalk.gray('Use:'));
48
+ console.log(chalk.white(' --show '), chalk.gray('Show current configuration'));
49
+ console.log(chalk.white(' --edit '), chalk.gray('Edit configuration file'));
50
+ } catch {
51
+ console.log(chalk.yellow('Configuration not found'));
52
+ console.log(
53
+ chalk.gray('\nRun: npx openclaw-watcher init')
54
+ );
55
+ }
56
+ }
57
+ }
@@ -0,0 +1,239 @@
1
+ import inquirer from 'inquirer';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import boxen from 'boxen';
5
+ import fs from 'fs/promises';
6
+ import path from 'path';
7
+ import os from 'os';
8
+ import { ConfigInitializer } from '@/setup/config-initializer.js';
9
+ import { GitInitializer } from '@/setup/git-initializer.js';
10
+ import { SafeConfigGenerator } from '@/setup/safe-config-generator.js';
11
+ import { GitHubCli } from '@/utils/github-cli.js';
12
+ import { getConfigPath } from '@/utils/paths.js';
13
+
14
+ interface InitOptions {
15
+ git?: boolean;
16
+ }
17
+
18
+ export async function initCommand(options: InitOptions) {
19
+ console.log(
20
+ boxen(chalk.bold.cyan('OpenClaw Watcher'), {
21
+ padding: 1,
22
+ margin: 1,
23
+ borderStyle: 'round',
24
+ borderColor: 'cyan',
25
+ })
26
+ );
27
+
28
+ console.log(chalk.gray('AI-powered health monitoring and auto-repair for OpenClaw Gateway\n'));
29
+
30
+ const answers = await inquirer.prompt([
31
+ {
32
+ type: 'input',
33
+ name: 'openclawConfigPath',
34
+ message: 'OpenClaw configuration directory:',
35
+ default: path.join(os.homedir(), '.openclaw'),
36
+ validate: async (input: string) => {
37
+ const expandedPath = input.replace(/^~/, os.homedir());
38
+ try {
39
+ const stats = await fs.stat(expandedPath);
40
+ if (!stats.isDirectory()) {
41
+ return 'Path must be a directory';
42
+ }
43
+ return true;
44
+ } catch {
45
+ return 'Directory does not exist';
46
+ }
47
+ },
48
+ },
49
+ {
50
+ type: 'input',
51
+ name: 'gatewayUrl',
52
+ message: 'OpenClaw Gateway URL:',
53
+ default: 'http://localhost:10002',
54
+ validate: (input: string) => {
55
+ try {
56
+ new URL(input);
57
+ return true;
58
+ } catch {
59
+ return 'Please enter a valid URL';
60
+ }
61
+ },
62
+ },
63
+ {
64
+ type: 'list',
65
+ name: 'aiProvider',
66
+ message: 'AI provider:',
67
+ choices: [
68
+ { name: 'Claude Code CLI (Recommended)', value: 'claude' },
69
+ { name: 'Kimi Code CLI', value: 'kimi' },
70
+ ],
71
+ default: 'claude',
72
+ },
73
+ {
74
+ type: 'confirm',
75
+ name: 'initGit',
76
+ message: 'Initialize Git repository for config tracking?',
77
+ default: options.git !== false,
78
+ when: () => options.git !== false,
79
+ },
80
+ {
81
+ type: 'input',
82
+ name: 'gitRepoName',
83
+ message: 'Git repository name:',
84
+ default: 'openclaw-config',
85
+ when: (answers: any) => answers.initGit,
86
+ },
87
+ {
88
+ type: 'confirm',
89
+ name: 'useGitHubCli',
90
+ message: 'Sync repair history to GitHub? (requires gh CLI)',
91
+ default: true,
92
+ when: (answers: any) => answers.initGit,
93
+ },
94
+ ]);
95
+
96
+ console.log('');
97
+ const spinner = ora('Initializing configuration...').start();
98
+
99
+ try {
100
+ // Expand paths
101
+ const openclawConfigPath = answers.openclawConfigPath.replace(/^~/, os.homedir());
102
+
103
+ // 1. Create config file
104
+ spinner.text = 'Creating configuration file...';
105
+ const configInitializer = new ConfigInitializer();
106
+ await configInitializer.create({
107
+ openclawConfigPath,
108
+ gatewayUrl: answers.gatewayUrl,
109
+ healthCheckInterval: 30000,
110
+ failureThreshold: 3,
111
+ aiProvider: answers.aiProvider,
112
+ gitTracking: answers.initGit || false,
113
+ useGitHubCli: answers.useGitHubCli || false,
114
+ });
115
+
116
+ // 2. Initialize Git repository if requested
117
+ if (answers.initGit) {
118
+ spinner.text = 'Initializing Git repository...';
119
+ const gitInitializer = new GitInitializer(openclawConfigPath);
120
+ await gitInitializer.init({
121
+ repoName: answers.gitRepoName,
122
+ isPrivate: true,
123
+ });
124
+
125
+ // 3. Generate safe config
126
+ spinner.text = 'Generating safe configuration file...';
127
+ const safeConfigGen = new SafeConfigGenerator(openclawConfigPath);
128
+ await safeConfigGen.generate();
129
+
130
+ // 4. Setup pre-commit hook
131
+ spinner.text = 'Setting up pre-commit hook...';
132
+ await gitInitializer.setupPreCommitHook();
133
+
134
+ // 5. Initial commit
135
+ spinner.text = 'Creating initial commit...';
136
+ await gitInitializer.initialCommit();
137
+
138
+ // 6. GitHub sync if requested
139
+ if (answers.useGitHubCli) {
140
+ await setupGitHubSync(spinner, answers.gitRepoName, openclawConfigPath);
141
+ }
142
+ }
143
+
144
+ spinner.succeed(chalk.green('Initialization complete!'));
145
+
146
+ console.log('');
147
+ console.log(
148
+ boxen(
149
+ chalk.white.bold('Next Steps:\n\n') +
150
+ chalk.cyan('1. ') +
151
+ 'Review configuration: ' +
152
+ chalk.yellow(getConfigPath()) +
153
+ '\n' +
154
+ chalk.cyan('2. ') +
155
+ 'Start monitoring: ' +
156
+ chalk.yellow('npx openclaw-watcher start') +
157
+ '\n' +
158
+ chalk.cyan('3. ') +
159
+ 'Check status: ' +
160
+ chalk.yellow('npx openclaw-watcher status'),
161
+ {
162
+ padding: 1,
163
+ margin: 1,
164
+ borderStyle: 'round',
165
+ borderColor: 'green',
166
+ }
167
+ )
168
+ );
169
+
170
+ if (answers.initGit) {
171
+ console.log(
172
+ chalk.gray(`\n📂 Git repository initialized at: ${chalk.white(openclawConfigPath)}`)
173
+ );
174
+ console.log(
175
+ chalk.gray(` - ${chalk.white('openclaw.safe.json')} is tracked (sensitive data redacted)`)
176
+ );
177
+ console.log(chalk.gray(` - ${chalk.white('openclaw.json')} is ignored (contains secrets)`));
178
+ if (answers.useGitHubCli) {
179
+ console.log(
180
+ chalk.gray(` - ${chalk.white('GitHub sync')} enabled (auto-push after each repair)`)
181
+ );
182
+ }
183
+ }
184
+ } catch (error: any) {
185
+ spinner.fail(chalk.red('Initialization failed'));
186
+ console.error(chalk.red(`\nError: ${error.message}`));
187
+ process.exit(1);
188
+ }
189
+ }
190
+
191
+ async function setupGitHubSync(spinner: ReturnType<typeof ora>, repoName: string, localPath: string): Promise<void> {
192
+ const gh = new GitHubCli();
193
+
194
+ // Check gh CLI installed
195
+ spinner.text = 'Checking GitHub CLI...';
196
+ if (!(await gh.isInstalled())) {
197
+ spinner.warn(chalk.yellow('GitHub CLI (gh) not found — skipping GitHub sync'));
198
+ console.log(chalk.gray(' Install: https://cli.github.com'));
199
+ spinner.start('Continuing initialization...');
200
+ return;
201
+ }
202
+
203
+ // Check gh auth
204
+ if (!(await gh.isAuthenticated())) {
205
+ spinner.stop();
206
+ console.log(chalk.yellow('\n⚠ GitHub CLI not authenticated.'));
207
+ console.log(chalk.gray(' Run `gh auth login` in another terminal, then continue.\n'));
208
+
209
+ const { retry } = await inquirer.prompt([
210
+ {
211
+ type: 'confirm',
212
+ name: 'retry',
213
+ message: 'Have you completed gh auth login?',
214
+ default: true,
215
+ },
216
+ ]);
217
+
218
+ spinner.start('Checking authentication...');
219
+
220
+ if (!retry || !(await gh.isAuthenticated())) {
221
+ spinner.warn(chalk.yellow('GitHub auth not ready — skipping GitHub sync'));
222
+ spinner.start('Continuing initialization...');
223
+ return;
224
+ }
225
+ }
226
+
227
+ // Create repo and push
228
+ spinner.text = 'Creating GitHub repository...';
229
+ const result = await gh.createRepoAndPush(repoName, localPath);
230
+
231
+ if (result.success) {
232
+ spinner.succeed(chalk.green(`GitHub repository created: ${repoName}`));
233
+ spinner.start('Finalizing...');
234
+ } else {
235
+ spinner.warn(chalk.yellow(`GitHub repo creation failed — you can set it up later`));
236
+ console.log(chalk.gray(` ${result.stderr}`));
237
+ spinner.start('Continuing initialization...');
238
+ }
239
+ }
@@ -0,0 +1,75 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { loadConfig } from '@/setup/config-loader.js';
4
+ import { GatewayMonitor } from '@/healthcheck/gateway-monitor.js';
5
+ import logger from '@/utils/logger.js';
6
+
7
+ interface StartOptions {
8
+ daemon?: boolean;
9
+ }
10
+
11
+ export async function startCommand(_options: StartOptions) {
12
+ const spinner = ora('Loading configuration...').start();
13
+
14
+ try {
15
+ const config = await loadConfig();
16
+
17
+ if (!config) {
18
+ spinner.fail(chalk.red('Configuration not found'));
19
+ console.log(
20
+ chalk.yellow(
21
+ '\nPlease run initialization first: npx openclaw-watcher init'
22
+ )
23
+ );
24
+ process.exit(1);
25
+ }
26
+
27
+ spinner.text = 'Starting OpenClaw Watcher...';
28
+
29
+ const monitor = new GatewayMonitor(
30
+ config.monitor,
31
+ config.ai,
32
+ config.recovery
33
+ );
34
+
35
+ await monitor.start();
36
+
37
+ spinner.succeed(chalk.green('OpenClaw Watcher started successfully!'));
38
+
39
+ console.log('');
40
+ console.log(chalk.cyan('📊 Monitoring:'), chalk.white(config.monitor.gatewayUrl));
41
+ console.log(
42
+ chalk.cyan('⏱️ Check interval:'),
43
+ chalk.white(`${config.monitor.checkInterval / 1000}s`)
44
+ );
45
+ console.log(
46
+ chalk.cyan('🚨 Failure threshold:'),
47
+ chalk.white(config.monitor.failureThreshold)
48
+ );
49
+ console.log(chalk.cyan('🤖 AI Provider:'), chalk.white(config.ai.provider));
50
+ console.log('');
51
+ console.log(chalk.gray('Press Ctrl+C to stop'));
52
+
53
+ // Handle graceful shutdown
54
+ process.on('SIGINT', () => {
55
+ console.log('\n');
56
+ const stopSpinner = ora('Stopping OpenClaw Watcher...').start();
57
+ monitor.stop();
58
+ stopSpinner.succeed(chalk.green('Stopped'));
59
+ process.exit(0);
60
+ });
61
+
62
+ process.on('SIGTERM', () => {
63
+ console.log('\n');
64
+ const stopSpinner = ora('Stopping OpenClaw Watcher...').start();
65
+ monitor.stop();
66
+ stopSpinner.succeed(chalk.green('Stopped'));
67
+ process.exit(0);
68
+ });
69
+ } catch (error: any) {
70
+ spinner.fail(chalk.red('Failed to start'));
71
+ console.error(chalk.red(`\nError: ${error.message}`));
72
+ logger.error('Failed to start', { error: error.message });
73
+ process.exit(1);
74
+ }
75
+ }
@@ -0,0 +1,79 @@
1
+ import chalk from 'chalk';
2
+ import { loadConfig } from '@/setup/config-loader.js';
3
+ import fs from 'fs/promises';
4
+ import { getChangesFilePath } from '@/utils/paths.js';
5
+
6
+ export async function statusCommand() {
7
+ try {
8
+ const config = await loadConfig();
9
+
10
+ if (!config) {
11
+ console.log(chalk.yellow('⚠️ Not initialized'));
12
+ console.log(
13
+ chalk.gray('\nRun: npx openclaw-watcher init')
14
+ );
15
+ return;
16
+ }
17
+
18
+ console.log(chalk.bold.cyan('\n📊 OpenClaw Watcher Status\n'));
19
+
20
+ console.log(chalk.white('Configuration:'));
21
+ console.log(chalk.gray(' Gateway URL:'), chalk.white(config.monitor.gatewayUrl));
22
+ console.log(
23
+ chalk.gray(' Check Interval:'),
24
+ chalk.white(`${config.monitor.checkInterval / 1000}s`)
25
+ );
26
+ console.log(
27
+ chalk.gray(' Failure Threshold:'),
28
+ chalk.white(config.monitor.failureThreshold)
29
+ );
30
+ console.log(chalk.gray(' AI Provider:'), chalk.white(config.ai.provider));
31
+ console.log(
32
+ chalk.gray(' Config Path:'),
33
+ chalk.white(config.recovery.openclawConfigPath)
34
+ );
35
+ console.log(
36
+ chalk.gray(' Git Tracking:'),
37
+ config.recovery.useGitTracking ? chalk.green('✓ Enabled') : chalk.gray('✗ Disabled')
38
+ );
39
+
40
+ // Check repair history
41
+ const changeLogPath = getChangesFilePath();
42
+ try {
43
+ const changeLogContent = await fs.readFile(changeLogPath, 'utf-8');
44
+ const changes = JSON.parse(changeLogContent);
45
+
46
+ console.log(chalk.white('\nRepair History:'));
47
+ console.log(
48
+ chalk.gray(' Total Repairs:'),
49
+ chalk.white(changes.length)
50
+ );
51
+
52
+ if (changes.length > 0) {
53
+ const lastChange = changes[changes.length - 1];
54
+ console.log(
55
+ chalk.gray(' Last Repair:'),
56
+ chalk.white(new Date(lastChange.timestamp).toLocaleString())
57
+ );
58
+ console.log(
59
+ chalk.gray(' Last Issue:'),
60
+ chalk.white(lastChange.diagnosis.issue)
61
+ );
62
+ console.log(
63
+ chalk.gray(' Success:'),
64
+ lastChange.recovery.success
65
+ ? chalk.green('✓ Yes')
66
+ : chalk.red('✗ No')
67
+ );
68
+ }
69
+ } catch {
70
+ console.log(chalk.white('\nRepair History:'));
71
+ console.log(chalk.gray(' No repairs recorded yet'));
72
+ }
73
+
74
+ console.log('');
75
+ } catch (error: any) {
76
+ console.error(chalk.red(`Error: ${error.message}`));
77
+ process.exit(1);
78
+ }
79
+ }
@@ -0,0 +1,25 @@
1
+ import { MonitorConfig, RecoveryConfig, AIClientConfig } from '@/types';
2
+
3
+ export const defaultMonitorConfig: MonitorConfig = {
4
+ gatewayUrl: process.env.OPENCLAW_GATEWAY_URL || 'http://localhost:10002',
5
+ healthEndpoint: process.env.OPENCLAW_HEALTH_ENDPOINT || '/',
6
+ checkInterval: parseInt(process.env.HEALTH_CHECK_INTERVAL || '30000', 10),
7
+ timeout: parseInt(process.env.HEALTH_CHECK_TIMEOUT || '10000', 10),
8
+ maxRetries: parseInt(process.env.MAX_RETRY_ATTEMPTS || '3', 10),
9
+ failureThreshold: parseInt(process.env.FAILURE_THRESHOLD || '3', 10),
10
+ };
11
+
12
+ export const defaultRecoveryConfig: RecoveryConfig = {
13
+ useGitTracking: process.env.USE_GIT_TRACKING === 'true',
14
+ useGitHubCli: process.env.USE_GITHUB_CLI === 'true',
15
+ gitCommitPrefix: process.env.GIT_COMMIT_MESSAGE_PREFIX || '[AutoFix]',
16
+ backupBeforeChange: process.env.BACKUP_CONFIG_BEFORE_CHANGE !== 'false',
17
+ openclawConfigPath: process.env.OPENCLAW_CONFIG_PATH || '~/.openclaw',
18
+ maxRecoveryRetries: parseInt(process.env.MAX_RECOVERY_RETRIES || '3', 10),
19
+ recoveryCooldownMs: parseInt(process.env.RECOVERY_COOLDOWN_MS || '300000', 10), // 5 minutes
20
+ };
21
+
22
+ export const defaultAIConfig: AIClientConfig = {
23
+ provider: (process.env.AI_PROVIDER as AIClientConfig['provider']) || 'claude',
24
+ timeout: 300000, // 5 minutes for AI to complete the fix
25
+ };
@@ -0,0 +1,137 @@
1
+ import cron from 'node-cron';
2
+ import { HealthChecker } from './health-checker.js';
3
+ import { AIOrchestrator } from '@/ai/ai-orchestrator.js';
4
+ import { AutoFixer } from '@/recovery/auto-fixer.js';
5
+ import logger from '@/utils/logger.js';
6
+ import { MonitorConfig, AIClientConfig, RecoveryConfig } from '@/types';
7
+
8
+ export class GatewayMonitor {
9
+ private healthChecker: HealthChecker;
10
+ private aiOrchestrator: AIOrchestrator;
11
+ private autoFixer: AutoFixer;
12
+ private cronJob: cron.ScheduledTask | null = null;
13
+ private isRecovering: boolean = false;
14
+ private consecutiveRecoveryFailures: number = 0;
15
+ private lastRecoveryEndTime: number = 0;
16
+ private maxRecoveryRetries: number;
17
+ private recoveryCooldownMs: number;
18
+
19
+ constructor(
20
+ monitorConfig: MonitorConfig,
21
+ aiConfig: AIClientConfig,
22
+ recoveryConfig: RecoveryConfig
23
+ ) {
24
+ this.healthChecker = new HealthChecker(monitorConfig);
25
+ this.aiOrchestrator = new AIOrchestrator(aiConfig, recoveryConfig);
26
+ this.autoFixer = new AutoFixer(recoveryConfig);
27
+ this.maxRecoveryRetries = recoveryConfig.maxRecoveryRetries;
28
+ this.recoveryCooldownMs = recoveryConfig.recoveryCooldownMs;
29
+ }
30
+
31
+ async start(): Promise<void> {
32
+ logger.info('Starting Gateway Monitor');
33
+
34
+ // Initialize AI orchestrator
35
+ await this.aiOrchestrator.initialize();
36
+
37
+ // Run initial health check
38
+ await this.performHealthCheck();
39
+
40
+ // Schedule periodic health checks
41
+ const intervalMs = this.healthChecker['config'].checkInterval;
42
+ const cronExpression = this.convertMsToCron(intervalMs);
43
+
44
+ this.cronJob = cron.schedule(cronExpression, async () => {
45
+ await this.performHealthCheck();
46
+ });
47
+
48
+ logger.info('Gateway Monitor started', { interval: intervalMs });
49
+ }
50
+
51
+ private async performHealthCheck(): Promise<void> {
52
+ if (this.isRecovering) {
53
+ logger.info('Recovery in progress, skipping health check');
54
+ return;
55
+ }
56
+
57
+ if (this.consecutiveRecoveryFailures >= this.maxRecoveryRetries) {
58
+ logger.warn('Max recovery retries reached, manual intervention required', {
59
+ consecutiveFailures: this.consecutiveRecoveryFailures,
60
+ maxRetries: this.maxRecoveryRetries,
61
+ });
62
+ return;
63
+ }
64
+
65
+ const cooldownRemaining = this.lastRecoveryEndTime + this.recoveryCooldownMs - Date.now();
66
+ if (cooldownRemaining > 0) {
67
+ logger.info('Recovery cooldown active, skipping health check', {
68
+ cooldownRemainingMs: cooldownRemaining,
69
+ });
70
+ return;
71
+ }
72
+
73
+ const result = await this.healthChecker.check();
74
+
75
+ if (!result.healthy && this.healthChecker.shouldTriggerRecovery()) {
76
+ logger.warn('Health check threshold exceeded, triggering recovery', {
77
+ consecutiveFailures: this.healthChecker.getConsecutiveFailures(),
78
+ recoveryAttempt: this.consecutiveRecoveryFailures + 1,
79
+ maxRetries: this.maxRecoveryRetries,
80
+ });
81
+ await this.triggerRecovery(result.error || 'Unknown error');
82
+ }
83
+ }
84
+
85
+ private async triggerRecovery(error: string): Promise<void> {
86
+ this.isRecovering = true;
87
+
88
+ try {
89
+ logger.info('Starting AI-powered recovery process', { error });
90
+
91
+ // AI directly diagnoses and fixes the problem
92
+ const diagnosis = await this.aiOrchestrator.diagnoseAndFix(error);
93
+ logger.info('AI diagnosis and fix completed', diagnosis);
94
+
95
+ // Record the fix (AI already applied it)
96
+ const recovery = await this.autoFixer.recordFix(diagnosis);
97
+ logger.info('Fix recorded', recovery);
98
+
99
+ if (recovery.success) {
100
+ this.healthChecker.reset();
101
+ this.consecutiveRecoveryFailures = 0;
102
+ logger.info('Recovery successful, resetting failure counter');
103
+ } else {
104
+ this.consecutiveRecoveryFailures++;
105
+ logger.error('Recovery verification failed', {
106
+ errors: recovery.errors,
107
+ consecutiveFailures: this.consecutiveRecoveryFailures,
108
+ });
109
+ }
110
+ } catch (error: any) {
111
+ this.consecutiveRecoveryFailures++;
112
+ logger.error('Recovery process failed', {
113
+ error: error.message,
114
+ consecutiveFailures: this.consecutiveRecoveryFailures,
115
+ });
116
+ } finally {
117
+ this.isRecovering = false;
118
+ this.lastRecoveryEndTime = Date.now();
119
+ }
120
+ }
121
+
122
+ private convertMsToCron(ms: number): string {
123
+ const seconds = Math.floor(ms / 1000);
124
+ if (seconds < 60) {
125
+ return `*/${seconds} * * * * *`;
126
+ }
127
+ const minutes = Math.floor(seconds / 60);
128
+ return `*/${minutes} * * * *`;
129
+ }
130
+
131
+ stop(): void {
132
+ if (this.cronJob) {
133
+ this.cronJob.stop();
134
+ logger.info('Gateway Monitor stopped');
135
+ }
136
+ }
137
+ }