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
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
|
+
}
|