clawsql 0.2.0 → 0.2.2

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 (63) hide show
  1. package/README.md +25 -5
  2. package/dist/bin/clawsql.d.ts +1 -0
  3. package/dist/bin/clawsql.d.ts.map +1 -1
  4. package/dist/bin/clawsql.js +18 -2
  5. package/dist/bin/clawsql.js.map +1 -1
  6. package/dist/cli/agent/handler.d.ts +13 -31
  7. package/dist/cli/agent/handler.d.ts.map +1 -1
  8. package/dist/cli/agent/handler.js +107 -149
  9. package/dist/cli/agent/handler.js.map +1 -1
  10. package/dist/cli/agent/index.d.ts +2 -1
  11. package/dist/cli/agent/index.d.ts.map +1 -1
  12. package/dist/cli/agent/index.js +7 -1
  13. package/dist/cli/agent/index.js.map +1 -1
  14. package/dist/cli/agent/openclaw-integration.d.ts +83 -25
  15. package/dist/cli/agent/openclaw-integration.d.ts.map +1 -1
  16. package/dist/cli/agent/openclaw-integration.js +305 -194
  17. package/dist/cli/agent/openclaw-integration.js.map +1 -1
  18. package/dist/cli/commands/cleanup.d.ts.map +1 -1
  19. package/dist/cli/commands/cleanup.js +26 -15
  20. package/dist/cli/commands/cleanup.js.map +1 -1
  21. package/dist/cli/commands/doctor.d.ts +0 -4
  22. package/dist/cli/commands/doctor.d.ts.map +1 -1
  23. package/dist/cli/commands/doctor.js +330 -469
  24. package/dist/cli/commands/doctor.js.map +1 -1
  25. package/dist/cli/commands/install.d.ts +13 -0
  26. package/dist/cli/commands/install.d.ts.map +1 -0
  27. package/dist/cli/commands/install.js +286 -0
  28. package/dist/cli/commands/install.js.map +1 -0
  29. package/dist/cli/commands/openclaw.d.ts +9 -0
  30. package/dist/cli/commands/openclaw.d.ts.map +1 -0
  31. package/dist/cli/commands/openclaw.js +236 -0
  32. package/dist/cli/commands/openclaw.js.map +1 -0
  33. package/dist/cli/commands/start.d.ts.map +1 -1
  34. package/dist/cli/commands/start.js +342 -16
  35. package/dist/cli/commands/start.js.map +1 -1
  36. package/dist/cli/commands/status.d.ts.map +1 -1
  37. package/dist/cli/commands/status.js +71 -25
  38. package/dist/cli/commands/status.js.map +1 -1
  39. package/dist/cli/index.d.ts.map +1 -1
  40. package/dist/cli/index.js +4 -0
  41. package/dist/cli/index.js.map +1 -1
  42. package/dist/cli/repl.js +1 -1
  43. package/dist/cli/repl.js.map +1 -1
  44. package/dist/cli/utils/ai-config.d.ts +32 -0
  45. package/dist/cli/utils/ai-config.d.ts.map +1 -0
  46. package/dist/cli/utils/ai-config.js +78 -0
  47. package/dist/cli/utils/ai-config.js.map +1 -0
  48. package/dist/cli/utils/command-executor.d.ts.map +1 -1
  49. package/dist/cli/utils/command-executor.js +60 -23
  50. package/dist/cli/utils/command-executor.js.map +1 -1
  51. package/dist/cli/utils/docker-files.d.ts.map +1 -1
  52. package/dist/cli/utils/docker-files.js +2 -0
  53. package/dist/cli/utils/docker-files.js.map +1 -1
  54. package/dist/cli/utils/docker-prereq.d.ts +23 -0
  55. package/dist/cli/utils/docker-prereq.d.ts.map +1 -1
  56. package/dist/cli/utils/docker-prereq.js +70 -1
  57. package/dist/cli/utils/docker-prereq.js.map +1 -1
  58. package/docker/openclaw/entrypoint.sh +102 -0
  59. package/docker/openclaw/openclaw.json +4 -1
  60. package/docker/orchestrator/orchestrator-schema.sql +837 -0
  61. package/docker-compose.yml +22 -14
  62. package/init/metadata.sql +14 -2
  63. package/package.json +1 -1
@@ -3,158 +3,138 @@
3
3
  * ClawSQL CLI - Doctor Command
4
4
  *
5
5
  * Diagnoses system health and suggests fixes for common issues.
6
- * Similar to `brew doctor` or `npm doctor`.
7
6
  */
8
7
  Object.defineProperty(exports, "__esModule", { value: true });
9
8
  exports.doctorCommand = void 0;
10
9
  const components_js_1 = require("../ui/components.js");
11
10
  const child_process_1 = require("child_process");
12
11
  const openclaw_integration_js_1 = require("../agent/openclaw-integration.js");
13
- /**
14
- * Doctor command
15
- */
12
+ const docker_prereq_js_1 = require("../utils/docker-prereq.js");
13
+ const ai_config_js_1 = require("../utils/ai-config.js");
14
+ // ============================================================================
15
+ // Command Definition
16
+ // ============================================================================
16
17
  exports.doctorCommand = {
17
18
  name: 'doctor',
18
19
  description: 'Diagnose system issues and suggest fixes',
19
20
  usage: '/doctor [--fix]',
20
- handler: async (args, ctx) => {
21
- const formatter = ctx.formatter;
22
- const shouldFix = args.includes('--fix');
23
- console.log(formatter.header('ClawSQL Doctor'));
21
+ handler: async (_args, ctx) => {
22
+ console.log(ctx.formatter.header('ClawSQL Doctor'));
24
23
  console.log(components_js_1.theme.muted('Running diagnostics...\n'));
25
24
  const results = [];
26
- // Run all diagnostic checks
27
25
  await runDiagnostics(ctx, results);
28
- // Display results
29
26
  displayResults(results);
30
- // Summary
31
- const errors = results.filter(r => r.severity === 'error');
32
- const warnings = results.filter(r => r.severity === 'warning');
33
- console.log();
34
- if (errors.length === 0 && warnings.length === 0) {
35
- console.log(components_js_1.theme.success(`${components_js_1.indicators.check} All systems healthy!`));
36
- }
37
- else {
38
- console.log(components_js_1.theme.warning(`Found ${errors.length} error(s) and ${warnings.length} warning(s)`));
39
- if (!shouldFix && (errors.length > 0 || warnings.length > 0)) {
40
- console.log(components_js_1.theme.muted('\nSome issues may have automatic fixes available.'));
41
- }
42
- }
27
+ printSummary(results);
43
28
  },
44
29
  };
45
- /**
46
- * Run all diagnostic checks
47
- */
30
+ // ============================================================================
31
+ // Diagnostic Runner
32
+ // ============================================================================
48
33
  async function runDiagnostics(ctx, results) {
49
- // Platform checks
50
- await checkContainerRuntime(results);
51
- await checkClawSQLAPI(ctx, results);
52
- await checkOrchestrator(ctx, results);
53
- await checkProxySQL(ctx, results);
54
- await checkPrometheus(ctx, results);
55
- await checkOpenClaw(ctx, results);
56
- // Configuration checks
34
+ const runtime = await checkContainerRuntime(results);
35
+ await Promise.all([
36
+ checkDockerImages(runtime, results),
37
+ checkClawSQLAPI(ctx, results),
38
+ checkOrchestrator(ctx, results),
39
+ checkProxySQL(ctx, results),
40
+ checkPrometheus(ctx, results),
41
+ checkOpenClaw(ctx, results),
42
+ ]);
57
43
  checkConfiguration(ctx, results);
58
- // MySQL checks
59
44
  await checkMySQLInstances(ctx, results);
60
45
  await checkReplicationTopology(ctx, results);
61
- // Sync check
62
46
  await checkProxySQLSync(ctx, results);
63
47
  }
64
- /**
65
- * Check container runtime availability
66
- */
48
+ // ============================================================================
49
+ // Individual Checks
50
+ // ============================================================================
67
51
  async function checkContainerRuntime(results) {
68
- const runtimes = ['docker', 'podman'];
69
- let foundRuntime = '';
70
- for (const runtime of runtimes) {
71
- try {
72
- const result = await execCommand([runtime, 'info'], true);
73
- if (result.success) {
74
- foundRuntime = runtime;
75
- break;
76
- }
77
- }
78
- catch {
79
- // Continue to next runtime
80
- }
81
- }
82
- if (foundRuntime) {
52
+ const runtime = await (0, docker_prereq_js_1.detectRuntime)();
53
+ if (!runtime) {
83
54
  results.push({
84
55
  name: 'Container Runtime',
85
- severity: 'ok',
86
- message: `${foundRuntime} is installed and running`,
56
+ severity: 'error',
57
+ message: 'No container runtime found (docker or podman required)',
58
+ fix: 'Install Docker from https://docs.docker.com/get-docker/',
59
+ });
60
+ return null;
61
+ }
62
+ results.push({
63
+ name: 'Container Runtime',
64
+ severity: 'ok',
65
+ message: `${runtime} is installed and running`,
66
+ });
67
+ const psResult = await execCommand([runtime, 'ps', '--filter', 'name=clawsql', '--filter', 'name=orchestrator', '--filter', 'name=proxysql', '-q'], true);
68
+ const containerCount = psResult.stdout.trim().split('\n').filter(Boolean).length;
69
+ if (containerCount === 0) {
70
+ results.push({
71
+ name: 'Platform Containers',
72
+ severity: 'warning',
73
+ message: 'No ClawSQL containers are running',
74
+ fix: 'Start the platform with: /start',
75
+ fixCommand: '/start',
87
76
  });
88
- // Check if containers are running
89
- try {
90
- const psResult = await execCommand([foundRuntime, 'ps', '--filter', 'name=clawsql', '--filter', 'name=orchestrator',
91
- '--filter', 'name=proxysql', '-q'], true);
92
- const containerCount = psResult.stdout.trim().split('\n').filter(Boolean).length;
93
- if (containerCount === 0) {
94
- results.push({
95
- name: 'Platform Containers',
96
- severity: 'warning',
97
- message: 'No ClawSQL containers are running',
98
- fix: 'Start the platform with: /start',
99
- fixCommand: '/start',
100
- });
101
- }
102
- else {
103
- results.push({
104
- name: 'Platform Containers',
105
- severity: 'ok',
106
- message: `${containerCount} container(s) running`,
107
- });
108
- }
109
- }
110
- catch {
111
- results.push({
112
- name: 'Platform Containers',
113
- severity: 'warning',
114
- message: 'Could not check container status',
115
- });
116
- }
117
77
  }
118
78
  else {
119
79
  results.push({
120
- name: 'Container Runtime',
80
+ name: 'Platform Containers',
81
+ severity: 'ok',
82
+ message: `${containerCount} container(s) running`,
83
+ });
84
+ }
85
+ return runtime;
86
+ }
87
+ async function checkDockerImages(runtime, results) {
88
+ if (!runtime)
89
+ return;
90
+ const status = await (0, docker_prereq_js_1.checkImagesInstalled)(runtime);
91
+ if (status.installed.length === status.total) {
92
+ results.push({ name: 'Docker Images', severity: 'ok', message: `All ${status.total} images installed` });
93
+ }
94
+ else if (status.installed.length === 0) {
95
+ results.push({
96
+ name: 'Docker Images',
121
97
  severity: 'error',
122
- message: 'No container runtime found (docker or podman required)',
123
- fix: 'Install Docker from https://docs.docker.com/get-docker/',
98
+ message: 'No Docker images installed',
99
+ fix: 'Pull images with: /install',
100
+ fixCommand: '/install',
101
+ });
102
+ }
103
+ else {
104
+ const missing = status.missing.slice(0, 3).map(i => i.split('/').pop()).join(', ');
105
+ results.push({
106
+ name: 'Docker Images',
107
+ severity: 'warning',
108
+ message: `${status.installed.length}/${status.total} images installed`,
109
+ detail: `Missing: ${missing}${status.missing.length > 3 ? '...' : ''}`,
110
+ fix: 'Pull missing images with: /install',
111
+ fixCommand: '/install',
124
112
  });
125
113
  }
126
114
  }
127
- /**
128
- * Check ClawSQL API health
129
- */
130
115
  async function checkClawSQLAPI(ctx, results) {
131
116
  try {
132
117
  const response = await fetch(`http://localhost:${ctx.settings.api.port}/health`, {
133
118
  signal: AbortSignal.timeout(5000),
134
119
  });
135
- if (response.ok) {
136
- const data = await response.json();
137
- if (data.status === 'healthy') {
138
- results.push({
139
- name: 'ClawSQL API',
140
- severity: 'ok',
141
- message: `Running on port ${ctx.settings.api.port}`,
142
- });
143
- }
144
- else {
145
- results.push({
146
- name: 'ClawSQL API',
147
- severity: 'error',
148
- message: `API returned status: ${data.status}`,
149
- fix: 'Check logs: docker logs clawsql',
150
- });
151
- }
120
+ if (!response.ok) {
121
+ results.push({
122
+ name: 'ClawSQL API',
123
+ severity: 'error',
124
+ message: `API returned HTTP ${response.status}`,
125
+ fix: 'Check logs: docker logs clawsql',
126
+ });
127
+ return;
128
+ }
129
+ const data = await response.json();
130
+ if (data.status === 'healthy') {
131
+ results.push({ name: 'ClawSQL API', severity: 'ok', message: `Running on port ${ctx.settings.api.port}` });
152
132
  }
153
133
  else {
154
134
  results.push({
155
135
  name: 'ClawSQL API',
156
136
  severity: 'error',
157
- message: `API returned HTTP ${response.status}`,
137
+ message: `API returned status: ${data.status}`,
158
138
  fix: 'Check logs: docker logs clawsql',
159
139
  });
160
140
  }
@@ -170,53 +150,18 @@ async function checkClawSQLAPI(ctx, results) {
170
150
  });
171
151
  }
172
152
  }
173
- /**
174
- * Check Orchestrator health
175
- */
176
153
  async function checkOrchestrator(ctx, results) {
154
+ // Health check first
177
155
  try {
178
156
  const isHealthy = await ctx.orchestrator.healthCheck();
179
- if (isHealthy) {
180
- results.push({
181
- name: 'Orchestrator',
182
- severity: 'ok',
183
- message: `Running at ${ctx.settings.orchestrator.url}`,
184
- });
185
- // Check if instances are discovered
186
- try {
187
- const clusters = await ctx.orchestrator.getClusters();
188
- if (clusters.length === 0) {
189
- results.push({
190
- name: 'MySQL Topology',
191
- severity: 'warning',
192
- message: 'No MySQL instances discovered in Orchestrator',
193
- fix: 'Register instances with: /instances register <host>',
194
- fixCommand: '/instances register <mysql-host>',
195
- });
196
- }
197
- else {
198
- results.push({
199
- name: 'MySQL Topology',
200
- severity: 'ok',
201
- message: `${clusters.length} cluster(s) discovered`,
202
- });
203
- }
204
- }
205
- catch {
206
- results.push({
207
- name: 'MySQL Topology',
208
- severity: 'warning',
209
- message: 'Could not retrieve topology information',
210
- });
211
- }
212
- }
213
- else {
157
+ if (!isHealthy) {
214
158
  results.push({
215
159
  name: 'Orchestrator',
216
160
  severity: 'error',
217
161
  message: 'Health check failed',
218
162
  fix: 'Check Orchestrator container: docker logs orchestrator',
219
163
  });
164
+ return;
220
165
  }
221
166
  }
222
167
  catch {
@@ -225,92 +170,89 @@ async function checkOrchestrator(ctx, results) {
225
170
  severity: 'error',
226
171
  message: 'Cannot connect to Orchestrator',
227
172
  detail: `Expected at ${ctx.settings.orchestrator.url}`,
228
- fix: 'Ensure Orchestrator container is running: docker ps | grep orchestrator',
173
+ fix: 'Ensure container is running: docker ps | grep orchestrator',
174
+ });
175
+ return;
176
+ }
177
+ // Orchestrator is healthy, add ok result
178
+ results.push({ name: 'Orchestrator', severity: 'ok', message: `Running at ${ctx.settings.orchestrator.url}` });
179
+ // Check for discovered clusters (separate try-catch to avoid duplicate error)
180
+ try {
181
+ const clusters = await ctx.orchestrator.getClusters();
182
+ if (clusters.length === 0) {
183
+ results.push({
184
+ name: 'MySQL Topology',
185
+ severity: 'warning',
186
+ message: 'No MySQL instances discovered in Orchestrator',
187
+ fix: 'Register instances with: /instances register <host>',
188
+ });
189
+ }
190
+ else {
191
+ results.push({ name: 'MySQL Topology', severity: 'ok', message: `${clusters.length} cluster(s) discovered` });
192
+ }
193
+ }
194
+ catch {
195
+ results.push({
196
+ name: 'MySQL Topology',
197
+ severity: 'warning',
198
+ message: 'Could not query Orchestrator for clusters',
199
+ fix: 'Check Orchestrator logs: docker logs orchestrator',
229
200
  });
230
201
  }
231
202
  }
232
- /**
233
- * Check ProxySQL health
234
- */
235
203
  async function checkProxySQL(ctx, results) {
236
204
  try {
237
205
  await ctx.proxysql.connect();
238
- results.push({
239
- name: 'ProxySQL',
240
- severity: 'ok',
241
- message: `Admin interface running on port ${ctx.settings.proxysql.adminPort}`,
242
- });
243
- // Check if MySQL servers are configured
244
- try {
245
- const servers = await ctx.proxysql.getServers();
246
- if (servers.length === 0) {
206
+ results.push({ name: 'ProxySQL', severity: 'ok', message: `Admin interface on port ${ctx.settings.proxysql.adminPort}` });
207
+ const servers = await ctx.proxysql.getServers();
208
+ if (servers.length === 0) {
209
+ results.push({
210
+ name: 'ProxySQL Servers',
211
+ severity: 'warning',
212
+ message: 'No MySQL servers configured',
213
+ fix: 'Register instances, then sync with: /clusters sync',
214
+ });
215
+ }
216
+ else {
217
+ const online = servers.filter(s => s.status === 'ONLINE').length;
218
+ if (online === servers.length) {
219
+ results.push({ name: 'ProxySQL Servers', severity: 'ok', message: `${online}/${servers.length} servers online` });
220
+ }
221
+ else {
247
222
  results.push({
248
223
  name: 'ProxySQL Servers',
249
224
  severity: 'warning',
250
- message: 'No MySQL servers configured in ProxySQL',
251
- fix: 'Register MySQL instances, then sync with: /clusters sync',
225
+ message: `${online}/${servers.length} servers online`,
226
+ fix: 'Check MySQL server connectivity and credentials',
252
227
  });
253
228
  }
254
- else {
255
- const onlineServers = servers.filter(s => s.status === 'ONLINE');
256
- if (onlineServers.length === servers.length) {
257
- results.push({
258
- name: 'ProxySQL Servers',
259
- severity: 'ok',
260
- message: `${onlineServers.length}/${servers.length} servers online`,
261
- });
262
- }
263
- else {
264
- results.push({
265
- name: 'ProxySQL Servers',
266
- severity: 'warning',
267
- message: `${onlineServers.length}/${servers.length} servers online`,
268
- fix: 'Check MySQL server connectivity and credentials',
269
- });
270
- }
271
- }
272
- }
273
- catch {
274
- results.push({
275
- name: 'ProxySQL Servers',
276
- severity: 'warning',
277
- message: 'Could not retrieve server configuration',
278
- });
279
229
  }
280
230
  await ctx.proxysql.close();
281
231
  }
282
232
  catch (error) {
283
- const message = error instanceof Error ? error.message : String(error);
284
233
  results.push({
285
234
  name: 'ProxySQL',
286
235
  severity: 'error',
287
236
  message: 'Cannot connect to ProxySQL admin interface',
288
- detail: message,
289
- fix: 'Ensure ProxySQL container is running: docker ps | grep proxysql',
237
+ detail: error instanceof Error ? error.message : String(error),
238
+ fix: 'Ensure container is running: docker ps | grep proxysql',
290
239
  });
291
240
  }
292
241
  }
293
- /**
294
- * Check Prometheus health
295
- */
296
242
  async function checkPrometheus(ctx, results) {
297
243
  try {
298
244
  const response = await fetch(`${ctx.settings.prometheus.url}/-/healthy`, {
299
245
  signal: AbortSignal.timeout(5000),
300
246
  });
301
247
  if (response.ok) {
302
- results.push({
303
- name: 'Prometheus',
304
- severity: 'ok',
305
- message: `Running at ${ctx.settings.prometheus.url}`,
306
- });
248
+ results.push({ name: 'Prometheus', severity: 'ok', message: `Running at ${ctx.settings.prometheus.url}` });
307
249
  }
308
250
  else {
309
251
  results.push({
310
252
  name: 'Prometheus',
311
253
  severity: 'warning',
312
254
  message: `Health check returned status ${response.status}`,
313
- fix: 'Check Prometheus container: docker logs prometheus',
255
+ fix: 'Check container: docker logs prometheus',
314
256
  });
315
257
  }
316
258
  }
@@ -324,127 +266,115 @@ async function checkPrometheus(ctx, results) {
324
266
  });
325
267
  }
326
268
  }
327
- /**
328
- * Check OpenClaw AI Gateway health
329
- */
330
269
  async function checkOpenClaw(_ctx, results) {
331
270
  try {
332
- const status = await (0, openclaw_integration_js_1.getOpenClawStatus)();
333
- if (status.available) {
334
- // Check gateway health
335
- const gatewayHealthy = await (0, openclaw_integration_js_1.isGatewayHealthy)();
336
- if (gatewayHealthy) {
337
- if (status.isDocker) {
338
- results.push({
339
- name: 'OpenClaw Gateway',
340
- severity: 'ok',
341
- message: 'Running in Docker container (ws://localhost:18789)',
342
- });
343
- }
344
- else if (status.isLocal) {
271
+ const status = await (0, openclaw_integration_js_1.getDetailedOpenClawStatus)();
272
+ if (!status.available) {
273
+ // Check if container exists but stopped
274
+ const runtime = await (0, docker_prereq_js_1.detectRuntime)();
275
+ if (runtime) {
276
+ const psResult = await execCommand([runtime, 'ps', '-a', '--filter', 'name=openclaw', '--format', '{{.Status}}'], true);
277
+ const containerStatus = psResult.stdout.trim();
278
+ if (containerStatus && !containerStatus.toLowerCase().includes('up')) {
345
279
  results.push({
346
280
  name: 'OpenClaw Gateway',
347
- severity: 'ok',
348
- message: 'Using local installation (ws://localhost:18789)',
281
+ severity: 'warning',
282
+ message: 'OpenClaw container exists but is not running',
283
+ detail: `Status: ${containerStatus}`,
284
+ fix: 'Restart the platform with: /start',
285
+ fixCommand: '/start',
349
286
  });
350
- }
351
- }
352
- else {
353
- results.push({
354
- name: 'OpenClaw Gateway',
355
- severity: 'warning',
356
- message: 'Gateway is not responding',
357
- detail: 'Container is running but gateway health check failed',
358
- fix: 'Check logs: docker logs openclaw',
359
- });
360
- }
361
- }
362
- else {
363
- // Check if Docker OpenClaw container exists but is not running
364
- const runtime = await detectRuntime();
365
- if (runtime) {
366
- try {
367
- const psResult = await execCommand([runtime, 'ps', '-a', '--filter', 'name=openclaw', '--format', '{{.Status}}'], true);
368
- const containerStatus = psResult.stdout.trim();
369
- if (containerStatus && !containerStatus.toLowerCase().includes('up')) {
370
- results.push({
371
- name: 'OpenClaw Gateway',
372
- severity: 'warning',
373
- message: 'OpenClaw container exists but is not running',
374
- detail: `Container status: ${containerStatus}`,
375
- fix: 'Restart the platform with: /start',
376
- fixCommand: '/start',
377
- });
378
- return;
379
- }
380
- }
381
- catch {
382
- // Continue to default message
287
+ return;
383
288
  }
384
289
  }
385
290
  results.push({
386
291
  name: 'OpenClaw Gateway',
387
292
  severity: 'warning',
388
293
  message: 'OpenClaw gateway is not available',
389
- detail: 'AI-powered features will not work without OpenClaw',
390
- fix: 'Start the platform with: /start to launch OpenClaw in Docker',
294
+ detail: 'AI features will not work without OpenClaw',
295
+ fix: 'Start with: /start',
391
296
  fixCommand: '/start',
392
297
  });
298
+ return;
299
+ }
300
+ if (!status.gatewayHealthy) {
301
+ results.push({
302
+ name: 'OpenClaw Gateway',
303
+ severity: 'warning',
304
+ message: 'Gateway is not responding',
305
+ fix: 'Check logs: docker logs openclaw',
306
+ });
307
+ return;
308
+ }
309
+ const modeDisplay = status.mode === 'docker' ? 'Running in Docker' : 'Using local installation';
310
+ results.push({ name: 'OpenClaw Gateway', severity: 'ok', message: `${modeDisplay} (ws://localhost:18789)` });
311
+ // Check for auto-detected AI config from environment
312
+ const aiConfig = (0, ai_config_js_1.detectAIConfigFromEnv)();
313
+ // Model info
314
+ if (status.modelInfo.configured && status.modelInfo.model) {
315
+ results.push({
316
+ name: 'OpenClaw Model',
317
+ severity: 'ok',
318
+ message: `${status.modelInfo.provider || 'custom'}: ${status.modelInfo.model}`,
319
+ });
320
+ }
321
+ else if (aiConfig.provider !== 'none') {
322
+ results.push({
323
+ name: 'OpenClaw Model',
324
+ severity: 'ok',
325
+ message: `${aiConfig.provider}${aiConfig.model ? '/' + aiConfig.model : ''} (auto-detected)`,
326
+ });
327
+ }
328
+ else {
329
+ results.push({
330
+ name: 'OpenClaw Model',
331
+ severity: 'info',
332
+ message: 'Using bundled qwen model (limited)',
333
+ fix: 'Set ANTHROPIC_API_KEY or OPENAI_API_KEY for better AI',
334
+ });
335
+ }
336
+ // Quick AI test
337
+ const testResult = await (0, openclaw_integration_js_1.testOpenClawConnection)('ping');
338
+ if (testResult.success) {
339
+ results.push({ name: 'OpenClaw AI Test', severity: 'ok', message: `AI responding (${testResult.latencyMs}ms)` });
340
+ }
341
+ else {
342
+ results.push({
343
+ name: 'OpenClaw AI Test',
344
+ severity: 'warning',
345
+ message: 'AI test failed',
346
+ detail: testResult.error,
347
+ fix: 'Check logs: docker logs openclaw',
348
+ });
393
349
  }
394
350
  }
395
351
  catch (error) {
396
- const message = error instanceof Error ? error.message : String(error);
397
352
  results.push({
398
353
  name: 'OpenClaw Gateway',
399
354
  severity: 'warning',
400
- message: 'Could not check OpenClaw status',
401
- detail: message,
402
- fix: 'Ensure OpenClaw container is running: docker ps | grep openclaw',
355
+ message: 'Could not check status',
356
+ detail: error instanceof Error ? error.message : String(error),
357
+ fix: 'Ensure container is running: docker ps | grep openclaw',
403
358
  });
404
359
  }
405
360
  }
406
- /**
407
- * Detect container runtime
408
- */
409
- async function detectRuntime() {
410
- const runtimes = ['docker', 'podman'];
411
- for (const runtime of runtimes) {
412
- try {
413
- const result = await execCommand([runtime, 'info'], true);
414
- if (result.success) {
415
- return runtime;
416
- }
417
- }
418
- catch {
419
- // Continue
420
- }
421
- }
422
- return null;
423
- }
424
- /**
425
- * Check configuration issues
426
- */
427
361
  function checkConfiguration(ctx, results) {
428
- // Check API token secret
429
362
  if (ctx.settings.api.tokenSecret === 'change-me-in-production') {
430
363
  results.push({
431
364
  name: 'API Token Secret',
432
365
  severity: 'warning',
433
366
  message: 'Using default token secret (not secure for production)',
434
- fix: 'Set environment variable: API_TOKEN_SECRET=<your-secret>',
367
+ fix: 'Set: API_TOKEN_SECRET=<your-secret>',
435
368
  });
436
369
  }
437
- // Check MySQL credentials
438
370
  if (!ctx.settings.mysql.adminPassword) {
439
371
  results.push({
440
372
  name: 'MySQL Credentials',
441
373
  severity: 'warning',
442
374
  message: 'MySQL admin password not configured',
443
- detail: 'Required for instance discovery and management',
444
- fix: 'Set environment variable: MYSQL_ADMIN_PASSWORD=<password>',
375
+ fix: 'Set: MYSQL_ADMIN_PASSWORD=<password>',
445
376
  });
446
377
  }
447
- // Check auto-failover
448
378
  if (!ctx.settings.failover.autoFailoverEnabled) {
449
379
  results.push({
450
380
  name: 'Auto Failover',
@@ -453,18 +383,10 @@ function checkConfiguration(ctx, results) {
453
383
  fix: 'Enable with: AUTO_FAILOVER_ENABLED=true',
454
384
  });
455
385
  }
456
- // Check metadata database
457
386
  if (!ctx.settings.metadataDb.host) {
458
- results.push({
459
- name: 'Metadata Database',
460
- severity: 'ok',
461
- message: 'Using auto-provisioned metadata-mysql container',
462
- });
387
+ results.push({ name: 'Metadata Database', severity: 'ok', message: 'Using auto-provisioned container' });
463
388
  }
464
389
  }
465
- /**
466
- * Check MySQL instances health
467
- */
468
390
  async function checkMySQLInstances(ctx, results) {
469
391
  try {
470
392
  const clusters = await ctx.orchestrator.getClusters();
@@ -472,74 +394,67 @@ async function checkMySQLInstances(ctx, results) {
472
394
  const topology = await ctx.orchestrator.getTopology(clusterName);
473
395
  if (!topology)
474
396
  continue;
397
+ const label = topology.name || clusterName;
475
398
  // Check primary
476
399
  if (topology.primary) {
477
400
  if (topology.primary.state !== 'online') {
478
401
  results.push({
479
- name: `Primary [${topology.name || clusterName}]`,
402
+ name: `Primary [${label}]`,
480
403
  severity: 'error',
481
- message: `Primary ${topology.primary.host}:${topology.primary.port} is ${topology.primary.state}`,
482
- fix: 'Check MySQL instance status and connectivity',
404
+ message: `${topology.primary.host}:${topology.primary.port} is ${topology.primary.state}`,
405
+ fix: 'Check MySQL instance status',
483
406
  });
484
407
  }
485
408
  else {
486
- results.push({
487
- name: `Primary [${topology.name || clusterName}]`,
488
- severity: 'ok',
489
- message: `${topology.primary.host}:${topology.primary.port} is online`,
490
- });
409
+ results.push({ name: `Primary [${label}]`, severity: 'ok', message: `${topology.primary.host}:${topology.primary.port} is online` });
491
410
  }
492
411
  }
493
412
  else {
494
413
  results.push({
495
- name: `Primary [${topology.name || clusterName}]`,
414
+ name: `Primary [${label}]`,
496
415
  severity: 'error',
497
- message: 'No primary found for cluster',
498
- fix: 'Check replication setup or promote a replica: /failover switchover',
416
+ message: 'No primary found',
417
+ fix: 'Check replication or promote: /failover switchover',
499
418
  });
500
419
  }
501
420
  // Check replicas
502
421
  for (const replica of topology.replicas) {
503
422
  if (replica.state !== 'online') {
504
423
  results.push({
505
- name: `Replica [${topology.name || clusterName}]`,
424
+ name: `Replica [${label}]`,
506
425
  severity: 'warning',
507
- message: `Replica ${replica.host}:${replica.port} is ${replica.state}`,
508
- fix: 'Check replica MySQL status and replication connection',
426
+ message: `${replica.host}:${replica.port} is ${replica.state}`,
427
+ fix: 'Check replica status',
509
428
  });
510
429
  }
511
430
  else if (replica.replicationLag !== undefined && replica.replicationLag > 60) {
512
431
  results.push({
513
- name: `Replica Lag [${topology.name || clusterName}]`,
432
+ name: `Replica Lag [${label}]`,
514
433
  severity: 'warning',
515
- message: `Replica ${replica.host}:${replica.port} has high lag (${replica.replicationLag}s)`,
516
- fix: 'Check replica performance and network connectivity',
434
+ message: `${replica.host}:${replica.port} lag: ${replica.replicationLag}s`,
435
+ fix: 'Check replica performance',
517
436
  });
518
437
  }
519
438
  }
520
439
  }
521
440
  }
522
441
  catch {
523
- // Already handled in Orchestrator check
442
+ // Handled in Orchestrator check
524
443
  }
525
444
  }
526
- /**
527
- * Check replication topology issues
528
- */
529
445
  async function checkReplicationTopology(ctx, results) {
530
446
  try {
531
447
  const analysis = await ctx.orchestrator.getReplicationAnalysis();
532
448
  for (const issue of analysis) {
533
449
  const analysisType = issue.Analysis;
534
- const description = issue.Description;
535
- const affectedHost = issue.Key?.Hostname;
536
450
  if (analysisType && !analysisType.includes('NoProblem')) {
451
+ const host = issue.Key?.Hostname;
537
452
  results.push({
538
453
  name: 'Replication Analysis',
539
454
  severity: 'warning',
540
- message: `${analysisType}: ${affectedHost || 'unknown'}`,
541
- detail: description,
542
- fix: 'Review Orchestrator UI for details: http://localhost:3000',
455
+ message: `${analysisType}: ${host || 'unknown'}`,
456
+ detail: issue.Description,
457
+ fix: 'Review Orchestrator UI: http://localhost:3000',
543
458
  });
544
459
  }
545
460
  }
@@ -548,238 +463,184 @@ async function checkReplicationTopology(ctx, results) {
548
463
  // Orchestrator may not be available
549
464
  }
550
465
  }
551
- /**
552
- * Check ProxySQL sync with Orchestrator topology
553
- */
554
466
  async function checkProxySQLSync(ctx, results) {
555
467
  try {
556
- // Get topology from Orchestrator
557
468
  const clusters = await ctx.orchestrator.getClusters();
558
- if (clusters.length === 0) {
559
- return; // No clusters to sync
560
- }
561
- // Get ProxySQL server stats and hostgroups
562
- let proxysqlServers = [];
469
+ if (clusters.length === 0)
470
+ return;
471
+ // Get ProxySQL data
472
+ let servers = [];
563
473
  let hostgroups = [];
564
474
  try {
565
475
  await ctx.proxysql.connect();
566
- proxysqlServers = await ctx.proxysql.getServers();
476
+ servers = await ctx.proxysql.getServers();
567
477
  hostgroups = await ctx.proxysql.getReplicationHostgroups();
568
478
  await ctx.proxysql.close();
569
479
  }
570
480
  catch {
571
- // ProxySQL not available - already reported in checkProxySQL
572
481
  return;
573
482
  }
574
483
  // Build expected instances from Orchestrator
575
- const expectedInstances = new Map();
484
+ const expected = new Map();
576
485
  for (const clusterName of clusters) {
577
486
  const topology = await ctx.orchestrator.getTopology(clusterName);
578
487
  if (!topology)
579
488
  continue;
580
489
  if (topology.primary) {
581
- const key = `${topology.primary.host}:${topology.primary.port}`;
582
- expectedInstances.set(key, {
490
+ expected.set(`${topology.primary.host}:${topology.primary.port}`, {
583
491
  host: topology.primary.host,
584
492
  port: topology.primary.port,
585
493
  role: 'primary',
586
- cluster: topology.name || clusterName,
587
494
  });
588
495
  }
589
496
  for (const replica of topology.replicas) {
590
- const key = `${replica.host}:${replica.port}`;
591
- expectedInstances.set(key, {
497
+ expected.set(`${replica.host}:${replica.port}`, {
592
498
  host: replica.host,
593
499
  port: replica.port,
594
500
  role: 'replica',
595
- cluster: topology.name || clusterName,
596
501
  });
597
502
  }
598
503
  }
599
- // Build actual ProxySQL instances
600
- const proxysqlInstances = new Map();
601
- for (const server of proxysqlServers) {
602
- const key = `${server.hostname}:${server.port}`;
603
- proxysqlInstances.set(key, {
604
- host: server.hostname,
605
- port: server.port,
606
- hostgroup: server.hostgroupId,
607
- status: server.status,
608
- });
609
- }
610
- // Determine default hostgroups (coerce to numbers)
611
- const defaultWriterHG = hostgroups.length > 0 ? Number(hostgroups[0].writerHostgroup) : 10;
612
- const defaultReaderHG = hostgroups.length > 0 ? Number(hostgroups[0].readerHostgroup) : 20;
613
- // Check for missing instances in ProxySQL
614
- const missingInProxySQL = [];
615
- const wrongHostgroup = [];
616
- for (const [key, instance] of expectedInstances) {
617
- const proxysqlInstance = proxysqlInstances.get(key);
618
- if (!proxysqlInstance) {
619
- missingInProxySQL.push(`${key} (${instance.role})`);
504
+ // Build actual from ProxySQL
505
+ const actual = new Map();
506
+ for (const s of servers) {
507
+ actual.set(`${s.hostname}:${s.port}`, { hostgroup: s.hostgroupId });
508
+ }
509
+ const writerHG = hostgroups.length > 0 ? Number(hostgroups[0].writerHostgroup) : 10;
510
+ const readerHG = hostgroups.length > 0 ? Number(hostgroups[0].readerHostgroup) : 20;
511
+ // Check for issues
512
+ const missing = [];
513
+ const wrongHG = [];
514
+ const orphans = [];
515
+ for (const [key, inst] of expected) {
516
+ const proxy = actual.get(key);
517
+ if (!proxy) {
518
+ missing.push(`${key} (${inst.role})`);
620
519
  }
621
520
  else {
622
- // Check if hostgroup is correct (coerce both to numbers for comparison)
623
- const actualHG = Number(proxysqlInstance.hostgroup);
624
- const expectedHG = instance.role === 'primary' ? defaultWriterHG : defaultReaderHG;
625
- // Skip if both are NaN or if they match
626
- if (!isNaN(actualHG) && !isNaN(expectedHG) && actualHG !== expectedHG) {
627
- wrongHostgroup.push(`${key} is in hg:${actualHG}, expected hg:${expectedHG}`);
521
+ const expectedHG = inst.role === 'primary' ? writerHG : readerHG;
522
+ if (!isNaN(proxy.hostgroup) && !isNaN(expectedHG) && proxy.hostgroup !== expectedHG) {
523
+ wrongHG.push(`${key} in hg:${proxy.hostgroup}, expected hg:${expectedHG}`);
628
524
  }
629
525
  }
630
526
  }
631
- // Check for orphan instances in ProxySQL (not in Orchestrator)
632
- const orphanInstances = [];
633
- for (const [key, instance] of proxysqlInstances) {
634
- if (!expectedInstances.has(key)) {
635
- orphanInstances.push(`${key} (hg:${instance.hostgroup})`);
527
+ for (const [key, inst] of actual) {
528
+ if (!expected.has(key)) {
529
+ orphans.push(`${key} (hg:${inst.hostgroup})`);
636
530
  }
637
531
  }
638
- // Report findings
639
- if (missingInProxySQL.length > 0) {
532
+ // Report
533
+ if (missing.length > 0) {
640
534
  results.push({
641
535
  name: 'ProxySQL Sync',
642
536
  severity: 'warning',
643
- message: `${missingInProxySQL.length} instance(s) from Orchestrator missing in ProxySQL`,
644
- detail: missingInProxySQL.slice(0, 5).join(', ') + (missingInProxySQL.length > 5 ? '...' : ''),
645
- fix: 'Sync clusters to ProxySQL with: /clusters sync',
537
+ message: `${missing.length} instance(s) missing in ProxySQL`,
538
+ detail: missing.slice(0, 5).join(', '),
539
+ fix: 'Sync with: /clusters sync',
646
540
  fixCommand: '/clusters sync',
647
541
  });
648
542
  }
649
- if (wrongHostgroup.length > 0) {
543
+ if (wrongHG.length > 0) {
650
544
  results.push({
651
545
  name: 'ProxySQL Hostgroups',
652
546
  severity: 'warning',
653
- message: `${wrongHostgroup.length} instance(s) in wrong hostgroup`,
654
- detail: wrongHostgroup.slice(0, 3).join('; '),
655
- fix: 'Re-sync clusters to ProxySQL with: /clusters sync',
656
- fixCommand: '/clusters sync',
547
+ message: `${wrongHG.length} instance(s) in wrong hostgroup`,
548
+ detail: wrongHG.slice(0, 3).join('; '),
549
+ fix: 'Re-sync with: /clusters sync',
657
550
  });
658
551
  }
659
- if (orphanInstances.length > 0) {
552
+ if (orphans.length > 0) {
660
553
  results.push({
661
554
  name: 'ProxySQL Orphans',
662
555
  severity: 'warning',
663
- message: `${orphanInstances.length} instance(s) in ProxySQL not in Orchestrator`,
664
- detail: orphanInstances.slice(0, 3).join(', '),
665
- fix: 'Remove orphan servers from ProxySQL or register them in Orchestrator',
556
+ message: `${orphans.length} orphan instance(s) in ProxySQL`,
557
+ detail: orphans.slice(0, 3).join(', '),
558
+ fix: 'Remove orphans or register in Orchestrator',
666
559
  });
667
560
  }
668
- // All synced properly
669
- if (missingInProxySQL.length === 0 && wrongHostgroup.length === 0 && orphanInstances.length === 0 && expectedInstances.size > 0) {
670
- results.push({
671
- name: 'ProxySQL Sync',
672
- severity: 'ok',
673
- message: `All ${expectedInstances.size} instances properly synced`,
674
- });
561
+ if (missing.length === 0 && wrongHG.length === 0 && orphans.length === 0 && expected.size > 0) {
562
+ results.push({ name: 'ProxySQL Sync', severity: 'ok', message: `All ${expected.size} instances synced` });
675
563
  }
676
564
  }
677
- catch (error) {
678
- const message = error instanceof Error ? error.message : String(error);
679
- results.push({
680
- name: 'ProxySQL Sync',
681
- severity: 'warning',
682
- message: 'Could not verify ProxySQL sync',
683
- detail: message,
684
- });
565
+ catch {
566
+ // Already reported in other checks
685
567
  }
686
568
  }
687
- /**
688
- * Display diagnostic results
689
- */
569
+ // ============================================================================
570
+ // Display Utilities
571
+ // ============================================================================
690
572
  function displayResults(results) {
691
- // Group by severity
692
- const errors = results.filter(r => r.severity === 'error');
693
- const warnings = results.filter(r => r.severity === 'warning');
694
- const ok = results.filter(r => r.severity === 'ok');
695
- const info = results.filter(r => r.severity === 'info');
696
- // Display errors first
697
- if (errors.length > 0) {
698
- console.log(components_js_1.theme.error.bold('\n✗ Errors:\n'));
699
- for (const result of errors) {
700
- displayResult(result);
701
- }
573
+ const groups = {
574
+ error: [],
575
+ warning: [],
576
+ info: [],
577
+ ok: [],
578
+ };
579
+ for (const r of results) {
580
+ groups[r.severity].push(r);
702
581
  }
703
- // Display warnings
704
- if (warnings.length > 0) {
705
- console.log(components_js_1.theme.warning.bold('\n◆ Warnings:\n'));
706
- for (const result of warnings) {
707
- displayResult(result);
708
- }
582
+ if (groups.error.length > 0) {
583
+ console.log(components_js_1.theme.error.bold('✗ Errors:'));
584
+ groups.error.forEach(displayResult);
709
585
  }
710
- // Display info
711
- if (info.length > 0) {
712
- console.log(components_js_1.theme.info.bold('\n○ Information:\n'));
713
- for (const result of info) {
714
- displayResult(result);
715
- }
586
+ if (groups.warning.length > 0) {
587
+ console.log(components_js_1.theme.warning.bold('◆ Warnings:'));
588
+ groups.warning.forEach(displayResult);
716
589
  }
717
- // Display healthy checks
718
- if (ok.length > 0) {
719
- console.log(components_js_1.theme.success.bold('\n✓ Healthy:\n'));
720
- for (const result of ok) {
721
- console.log(components_js_1.theme.muted(` ${result.name}: `) + components_js_1.theme.success(result.message));
722
- }
590
+ if (groups.info.length > 0) {
591
+ console.log(components_js_1.theme.info.bold('○ Information:'));
592
+ groups.info.forEach(displayResult);
593
+ }
594
+ if (groups.ok.length > 0) {
595
+ console.log(components_js_1.theme.success.bold('✓ Healthy:'));
596
+ groups.ok.forEach(r => console.log(components_js_1.theme.muted(` ${r.name}: `) + components_js_1.theme.success(r.message)));
723
597
  }
724
598
  }
725
- /**
726
- * Display a single diagnostic result
727
- */
728
599
  function displayResult(result) {
729
- const severityStyles = {
730
- error: components_js_1.theme.error,
731
- warning: components_js_1.theme.warning,
732
- ok: components_js_1.theme.success,
733
- info: components_js_1.theme.info,
734
- };
735
- const severityIcons = {
600
+ const icons = {
736
601
  error: components_js_1.theme.error(components_js_1.indicators.cross),
737
602
  warning: components_js_1.theme.warning(components_js_1.indicators.warning),
738
- ok: components_js_1.theme.success(components_js_1.indicators.check),
739
603
  info: components_js_1.theme.info(components_js_1.indicators.info),
604
+ ok: components_js_1.theme.success(components_js_1.indicators.check),
740
605
  };
741
- const icon = severityIcons[result.severity];
742
- const nameColor = severityStyles[result.severity];
743
- console.log(` ${icon} ${nameColor.bold(result.name)}: ${result.message}`);
744
- if (result.detail) {
606
+ const colorFn = {
607
+ error: components_js_1.theme.error,
608
+ warning: components_js_1.theme.warning,
609
+ info: components_js_1.theme.info,
610
+ ok: components_js_1.theme.success,
611
+ }[result.severity];
612
+ console.log(` ${icons[result.severity]} ${colorFn.bold(result.name)}: ${result.message}`);
613
+ if (result.detail)
745
614
  console.log(components_js_1.theme.muted(` ${result.detail}`));
746
- }
747
- if (result.fix) {
615
+ if (result.fix)
748
616
  console.log(components_js_1.theme.primary(` Fix: ${result.fix}`));
749
- }
750
- if (result.fixCommand) {
617
+ if (result.fixCommand)
751
618
  console.log(components_js_1.theme.muted(` Command: ${result.fixCommand}`));
619
+ }
620
+ function printSummary(results) {
621
+ const errors = results.filter(r => r.severity === 'error').length;
622
+ const warnings = results.filter(r => r.severity === 'warning').length;
623
+ if (errors === 0 && warnings === 0) {
624
+ console.log(components_js_1.theme.success(`${components_js_1.indicators.check} All systems healthy!`));
625
+ }
626
+ else {
627
+ console.log(components_js_1.theme.warning(`Found ${errors} error(s) and ${warnings} warning(s)`));
752
628
  }
753
- console.log();
754
629
  }
755
- /**
756
- * Execute a shell command
757
- */
630
+ // ============================================================================
631
+ // Process Execution
632
+ // ============================================================================
758
633
  function execCommand(cmd, silent = false) {
759
634
  return new Promise((resolve) => {
760
- const proc = (0, child_process_1.spawn)(cmd[0], cmd.slice(1), {
761
- stdio: silent ? 'pipe' : 'inherit',
762
- });
635
+ const proc = (0, child_process_1.spawn)(cmd[0], cmd.slice(1), { stdio: silent ? 'pipe' : 'inherit' });
763
636
  let stdout = '';
764
637
  let stderr = '';
765
638
  if (silent) {
766
639
  proc.stdout?.on('data', (data) => { stdout += data; });
767
640
  proc.stderr?.on('data', (data) => { stderr += data; });
768
641
  }
769
- proc.on('close', (code) => {
770
- resolve({
771
- success: code === 0,
772
- stdout,
773
- stderr,
774
- });
775
- });
776
- proc.on('error', () => {
777
- resolve({
778
- success: false,
779
- stdout: '',
780
- stderr: 'Failed to execute command',
781
- });
782
- });
642
+ proc.on('close', (code) => resolve({ success: code === 0, stdout, stderr }));
643
+ proc.on('error', () => resolve({ success: false, stdout: '', stderr: 'Failed to execute' }));
783
644
  });
784
645
  }
785
646
  exports.default = exports.doctorCommand;