myshell-tools 1.0.0

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 (45) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/LICENSE +21 -0
  3. package/README.md +318 -0
  4. package/data/orchestrator.json +113 -0
  5. package/package.json +49 -0
  6. package/src/auth/recovery.mjs +328 -0
  7. package/src/auth/refresh.mjs +373 -0
  8. package/src/chef.mjs +348 -0
  9. package/src/cli/doctor.mjs +568 -0
  10. package/src/cli/reset.mjs +447 -0
  11. package/src/cli/status.mjs +379 -0
  12. package/src/cli.mjs +429 -0
  13. package/src/commands/doctor.mjs +375 -0
  14. package/src/commands/help.mjs +324 -0
  15. package/src/commands/status.mjs +331 -0
  16. package/src/monitor/health.mjs +486 -0
  17. package/src/monitor/performance.mjs +442 -0
  18. package/src/monitor/report.mjs +535 -0
  19. package/src/orchestrator/classify.mjs +391 -0
  20. package/src/orchestrator/confidence.mjs +151 -0
  21. package/src/orchestrator/handoffs.mjs +231 -0
  22. package/src/orchestrator/review.mjs +222 -0
  23. package/src/providers/balance.mjs +201 -0
  24. package/src/providers/claude.mjs +236 -0
  25. package/src/providers/codex.mjs +255 -0
  26. package/src/providers/detect.mjs +185 -0
  27. package/src/providers/errors.mjs +373 -0
  28. package/src/providers/select.mjs +162 -0
  29. package/src/repl-enhanced.mjs +417 -0
  30. package/src/repl.mjs +321 -0
  31. package/src/state/archive.mjs +366 -0
  32. package/src/state/atomic.mjs +116 -0
  33. package/src/state/cleanup.mjs +440 -0
  34. package/src/state/recovery.mjs +461 -0
  35. package/src/state/session.mjs +147 -0
  36. package/src/ui/errors.mjs +456 -0
  37. package/src/ui/formatter.mjs +327 -0
  38. package/src/ui/icons.mjs +318 -0
  39. package/src/ui/progress.mjs +468 -0
  40. package/templates/prompts/confidence-format.txt +14 -0
  41. package/templates/prompts/ic-with-feedback.txt +41 -0
  42. package/templates/prompts/ic.txt +13 -0
  43. package/templates/prompts/manager-review.txt +40 -0
  44. package/templates/prompts/manager.txt +14 -0
  45. package/templates/prompts/worker.txt +12 -0
@@ -0,0 +1,486 @@
1
+ /**
2
+ * health.mjs — System health checks and monitoring
3
+ */
4
+
5
+ import { spawnSync } from 'child_process';
6
+ import { existsSync } from 'fs';
7
+ import { join } from 'path';
8
+ import { atomicAppendJSONL, lockedReadModifyWrite } from '../state/atomic.mjs';
9
+
10
+ /**
11
+ * Health monitoring class
12
+ */
13
+ export class HealthMonitor {
14
+ constructor(workspace = process.cwd()) {
15
+ this.workspace = workspace;
16
+ this.healthDir = join(workspace, '.cortex', 'health');
17
+ this.startTime = Date.now();
18
+ this.checks = new Map();
19
+ }
20
+
21
+ /**
22
+ * Ensure health directory exists
23
+ */
24
+ ensureHealthDir() {
25
+ if (!existsSync(this.healthDir)) {
26
+ require('fs').mkdirSync(this.healthDir, { recursive: true });
27
+ }
28
+ return this.healthDir;
29
+ }
30
+
31
+ /**
32
+ * Check CLI availability and response time
33
+ */
34
+ async checkCliHealth(provider, command, args = ['--version']) {
35
+ const startTime = Date.now();
36
+
37
+ try {
38
+ const result = spawnSync(command, args, {
39
+ encoding: 'utf8',
40
+ stdio: 'pipe',
41
+ timeout: 10000 // 10 second timeout
42
+ });
43
+
44
+ const responseTime = Date.now() - startTime;
45
+
46
+ const healthData = {
47
+ provider,
48
+ command,
49
+ timestamp: Date.now(),
50
+ available: result.status === 0,
51
+ responseTime,
52
+ version: result.status === 0 ? result.stdout.trim().split('\n')[0] : null,
53
+ error: result.status !== 0 ? (result.stderr || 'Command failed') : null
54
+ };
55
+
56
+ this.recordHealthCheck(provider, healthData);
57
+ return healthData;
58
+
59
+ } catch (error) {
60
+ const responseTime = Date.now() - startTime;
61
+
62
+ const healthData = {
63
+ provider,
64
+ command,
65
+ timestamp: Date.now(),
66
+ available: false,
67
+ responseTime,
68
+ version: null,
69
+ error: error.code === 'ENOENT' ? 'Command not found' : error.message
70
+ };
71
+
72
+ this.recordHealthCheck(provider, healthData);
73
+ return healthData;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Check network connectivity to provider APIs
79
+ */
80
+ async checkNetworkHealth(provider, endpoint) {
81
+ return new Promise((resolve) => {
82
+ const startTime = Date.now();
83
+
84
+ try {
85
+ const https = require('https');
86
+ const url = new URL(endpoint);
87
+
88
+ const req = https.request({
89
+ hostname: url.hostname,
90
+ port: url.port || 443,
91
+ path: url.pathname,
92
+ method: 'HEAD',
93
+ timeout: 5000
94
+ }, (res) => {
95
+ const responseTime = Date.now() - startTime;
96
+
97
+ const healthData = {
98
+ provider,
99
+ endpoint,
100
+ timestamp: Date.now(),
101
+ available: res.statusCode < 400,
102
+ responseTime,
103
+ statusCode: res.statusCode,
104
+ error: res.statusCode >= 400 ? `HTTP ${res.statusCode}` : null
105
+ };
106
+
107
+ this.recordHealthCheck(`${provider}_network`, healthData);
108
+ resolve(healthData);
109
+ });
110
+
111
+ req.on('error', (error) => {
112
+ const responseTime = Date.now() - startTime;
113
+
114
+ const healthData = {
115
+ provider,
116
+ endpoint,
117
+ timestamp: Date.now(),
118
+ available: false,
119
+ responseTime,
120
+ statusCode: null,
121
+ error: error.message
122
+ };
123
+
124
+ this.recordHealthCheck(`${provider}_network`, healthData);
125
+ resolve(healthData);
126
+ });
127
+
128
+ req.on('timeout', () => {
129
+ const responseTime = Date.now() - startTime;
130
+
131
+ const healthData = {
132
+ provider,
133
+ endpoint,
134
+ timestamp: Date.now(),
135
+ available: false,
136
+ responseTime,
137
+ statusCode: null,
138
+ error: 'Timeout'
139
+ };
140
+
141
+ this.recordHealthCheck(`${provider}_network`, healthData);
142
+ resolve(healthData);
143
+ req.destroy();
144
+ });
145
+
146
+ req.end();
147
+ } catch (error) {
148
+ const responseTime = Date.now() - startTime;
149
+
150
+ const healthData = {
151
+ provider,
152
+ endpoint,
153
+ timestamp: Date.now(),
154
+ available: false,
155
+ responseTime,
156
+ statusCode: null,
157
+ error: error.message
158
+ };
159
+
160
+ this.recordHealthCheck(`${provider}_network`, healthData);
161
+ resolve(healthData);
162
+ }
163
+ });
164
+ }
165
+
166
+ /**
167
+ * Check system resources
168
+ */
169
+ checkSystemHealth() {
170
+ const memUsage = process.memoryUsage();
171
+
172
+ const healthData = {
173
+ timestamp: Date.now(),
174
+ memory: {
175
+ heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024), // MB
176
+ heapTotal: Math.round(memUsage.heapTotal / 1024 / 1024),
177
+ external: Math.round(memUsage.external / 1024 / 1024),
178
+ rss: Math.round(memUsage.rss / 1024 / 1024)
179
+ },
180
+ uptime: Math.round(process.uptime() * 1000), // ms
181
+ loadAverage: process.platform !== 'win32' ? process.loadavg() : null,
182
+ platform: process.platform,
183
+ arch: process.arch,
184
+ nodeVersion: process.version
185
+ };
186
+
187
+ this.recordHealthCheck('system', healthData);
188
+ return healthData;
189
+ }
190
+
191
+ /**
192
+ * Record health check result
193
+ */
194
+ recordHealthCheck(component, data) {
195
+ this.checks.set(component, data);
196
+
197
+ // Persist to file
198
+ this.ensureHealthDir();
199
+ const logPath = join(this.healthDir, `${component}-health.jsonl`);
200
+
201
+ try {
202
+ atomicAppendJSONL(logPath, data);
203
+ } catch (error) {
204
+ console.warn(`Failed to log health check for ${component}:`, error.message);
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Get recent health status for a component
210
+ */
211
+ getRecentHealth(component, timeWindowMs = 5 * 60 * 1000) {
212
+ const logPath = join(this.healthDir, `${component}-health.jsonl`);
213
+
214
+ if (!existsSync(logPath)) {
215
+ return [];
216
+ }
217
+
218
+ try {
219
+ const content = require('fs').readFileSync(logPath, 'utf8');
220
+ const cutoff = Date.now() - timeWindowMs;
221
+
222
+ return content
223
+ .trim()
224
+ .split('\n')
225
+ .filter(line => line.trim())
226
+ .map(line => {
227
+ try {
228
+ return JSON.parse(line);
229
+ } catch {
230
+ return null;
231
+ }
232
+ })
233
+ .filter(Boolean)
234
+ .filter(entry => entry.timestamp > cutoff)
235
+ .sort((a, b) => a.timestamp - b.timestamp);
236
+ } catch {
237
+ return [];
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Calculate component health score
243
+ */
244
+ calculateHealthScore(component, timeWindowMs = 15 * 60 * 1000) {
245
+ const recent = this.getRecentHealth(component, timeWindowMs);
246
+
247
+ if (recent.length === 0) {
248
+ return { score: 0, status: 'unknown', message: 'No recent data' };
249
+ }
250
+
251
+ const availabilityRatio = recent.filter(entry => entry.available).length / recent.length;
252
+ const avgResponseTime = recent
253
+ .filter(entry => entry.available && entry.responseTime)
254
+ .reduce((sum, entry) => sum + entry.responseTime, 0) / Math.max(recent.length, 1);
255
+
256
+ let score = availabilityRatio * 100;
257
+
258
+ // Penalize high response times
259
+ if (avgResponseTime > 5000) {
260
+ score *= 0.7; // High latency
261
+ } else if (avgResponseTime > 2000) {
262
+ score *= 0.9; // Moderate latency
263
+ }
264
+
265
+ const status = score >= 90 ? 'healthy' :
266
+ score >= 70 ? 'degraded' :
267
+ score >= 50 ? 'unhealthy' : 'critical';
268
+
269
+ return {
270
+ score: Math.round(score),
271
+ status,
272
+ availabilityRatio,
273
+ avgResponseTime: Math.round(avgResponseTime),
274
+ recentChecks: recent.length,
275
+ message: this.getHealthMessage(status, availabilityRatio, avgResponseTime)
276
+ };
277
+ }
278
+
279
+ /**
280
+ * Get health status message
281
+ */
282
+ getHealthMessage(status, availability, responseTime) {
283
+ switch (status) {
284
+ case 'healthy':
285
+ return `Healthy - ${(availability * 100).toFixed(1)}% availability`;
286
+
287
+ case 'degraded':
288
+ if (availability < 0.8) {
289
+ return `Degraded - ${(availability * 100).toFixed(1)}% availability`;
290
+ } else {
291
+ return `Degraded - High latency (${responseTime}ms)`;
292
+ }
293
+
294
+ case 'unhealthy':
295
+ return `Unhealthy - ${(availability * 100).toFixed(1)}% availability, ${responseTime}ms latency`;
296
+
297
+ case 'critical':
298
+ return `Critical - ${(availability * 100).toFixed(1)}% availability`;
299
+
300
+ default:
301
+ return 'Unknown status';
302
+ }
303
+ }
304
+
305
+ /**
306
+ * Comprehensive health check
307
+ */
308
+ async runComprehensiveCheck() {
309
+ const results = {
310
+ timestamp: Date.now(),
311
+ workspace: this.workspace,
312
+ components: {},
313
+ overall: { score: 0, status: 'unknown' }
314
+ };
315
+
316
+ // Check CLI health
317
+ results.components.claude = await this.checkCliHealth('claude', 'claude');
318
+ results.components.codex = await this.checkCliHealth('codex', 'codex');
319
+
320
+ // Check network health
321
+ results.components.claude_network = await this.checkNetworkHealth('claude', 'https://console.anthropic.com');
322
+ results.components.openai_network = await this.checkNetworkHealth('openai', 'https://api.openai.com');
323
+
324
+ // Check system health
325
+ results.components.system = this.checkSystemHealth();
326
+
327
+ // Calculate health scores
328
+ for (const [component, data] of Object.entries(results.components)) {
329
+ if (data.timestamp) {
330
+ const score = this.calculateHealthScore(component);
331
+ results.components[component].healthScore = score;
332
+ }
333
+ }
334
+
335
+ // Calculate overall score
336
+ const scores = Object.values(results.components)
337
+ .map(comp => comp.healthScore?.score || 0)
338
+ .filter(score => score > 0);
339
+
340
+ if (scores.length > 0) {
341
+ results.overall.score = Math.round(scores.reduce((sum, score) => sum + score, 0) / scores.length);
342
+ results.overall.status = results.overall.score >= 80 ? 'healthy' :
343
+ results.overall.score >= 60 ? 'degraded' :
344
+ results.overall.score >= 40 ? 'unhealthy' : 'critical';
345
+ }
346
+
347
+ // Update running health statistics
348
+ this.updateHealthStats(results);
349
+
350
+ return results;
351
+ }
352
+
353
+ /**
354
+ * Update running health statistics
355
+ */
356
+ updateHealthStats(results) {
357
+ const statsPath = join(this.healthDir, 'running-health-stats.json');
358
+
359
+ try {
360
+ return lockedReadModifyWrite(statsPath, (stats) => {
361
+ const updated = stats || {
362
+ totalChecks: 0,
363
+ averageScore: 0,
364
+ downtimeEvents: 0,
365
+ lastCheck: null,
366
+ components: {}
367
+ };
368
+
369
+ updated.totalChecks++;
370
+ updated.lastCheck = results.timestamp;
371
+
372
+ // Update overall average
373
+ const newScore = results.overall.score;
374
+ updated.averageScore = ((updated.averageScore * (updated.totalChecks - 1)) + newScore) / updated.totalChecks;
375
+
376
+ // Track component statistics
377
+ for (const [component, data] of Object.entries(results.components)) {
378
+ if (!updated.components[component]) {
379
+ updated.components[component] = {
380
+ checks: 0,
381
+ averageScore: 0,
382
+ downtimeEvents: 0,
383
+ lastDowntime: null
384
+ };
385
+ }
386
+
387
+ const compStats = updated.components[component];
388
+ compStats.checks++;
389
+
390
+ if (data.healthScore) {
391
+ compStats.averageScore = ((compStats.averageScore * (compStats.checks - 1)) + data.healthScore.score) / compStats.checks;
392
+
393
+ // Track downtime events
394
+ if (data.healthScore.status === 'critical' || data.healthScore.status === 'unhealthy') {
395
+ if (compStats.lastDowntime === null || (results.timestamp - compStats.lastDowntime) > 60 * 60 * 1000) {
396
+ compStats.downtimeEvents++;
397
+ compStats.lastDowntime = results.timestamp;
398
+ }
399
+ }
400
+ }
401
+ }
402
+
403
+ return updated;
404
+ });
405
+ } catch (error) {
406
+ console.warn('Failed to update health stats:', error.message);
407
+ return null;
408
+ }
409
+ }
410
+
411
+ /**
412
+ * Get health trends
413
+ */
414
+ getHealthTrends(component, timeWindowMs = 24 * 60 * 60 * 1000) {
415
+ const recent = this.getRecentHealth(component, timeWindowMs);
416
+
417
+ if (recent.length < 2) {
418
+ return { trend: 'insufficient_data', slope: 0 };
419
+ }
420
+
421
+ // Calculate availability trend over time
422
+ const dataPoints = recent.map((entry, index) => ({
423
+ x: index,
424
+ y: entry.available ? 1 : 0
425
+ }));
426
+
427
+ // Simple linear regression to determine trend
428
+ const n = dataPoints.length;
429
+ const sumX = dataPoints.reduce((sum, p) => sum + p.x, 0);
430
+ const sumY = dataPoints.reduce((sum, p) => sum + p.y, 0);
431
+ const sumXY = dataPoints.reduce((sum, p) => sum + (p.x * p.y), 0);
432
+ const sumXX = dataPoints.reduce((sum, p) => sum + (p.x * p.x), 0);
433
+
434
+ const slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX);
435
+
436
+ const trend = slope > 0.01 ? 'improving' :
437
+ slope < -0.01 ? 'declining' : 'stable';
438
+
439
+ return {
440
+ trend,
441
+ slope: slope.toFixed(4),
442
+ dataPoints: dataPoints.length,
443
+ availability: (sumY / n).toFixed(3)
444
+ };
445
+ }
446
+ }
447
+
448
+ /**
449
+ * Global health monitor instance
450
+ */
451
+ let globalHealthMonitor = null;
452
+
453
+ /**
454
+ * Get or create global health monitor
455
+ */
456
+ export function getHealthMonitor(workspace = process.cwd()) {
457
+ if (!globalHealthMonitor) {
458
+ globalHealthMonitor = new HealthMonitor(workspace);
459
+ }
460
+ return globalHealthMonitor;
461
+ }
462
+
463
+ /**
464
+ * Run health check
465
+ */
466
+ export async function runHealthCheck(workspace = process.cwd()) {
467
+ const monitor = getHealthMonitor(workspace);
468
+ return await monitor.runComprehensiveCheck();
469
+ }
470
+
471
+ /**
472
+ * Get component health score
473
+ */
474
+ export function getComponentHealth(component, workspace = process.cwd()) {
475
+ const monitor = getHealthMonitor(workspace);
476
+ return monitor.calculateHealthScore(component);
477
+ }
478
+
479
+ /**
480
+ * Check if a provider is healthy
481
+ */
482
+ export function isProviderHealthy(provider, workspace = process.cwd()) {
483
+ const monitor = getHealthMonitor(workspace);
484
+ const health = monitor.calculateHealthScore(provider);
485
+ return health.status === 'healthy' || health.status === 'degraded';
486
+ }