@ekkos/cli 0.3.3 → 1.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 (81) hide show
  1. package/README.md +57 -0
  2. package/dist/agent/daemon.d.ts +27 -0
  3. package/dist/agent/daemon.js +254 -29
  4. package/dist/agent/health-check.d.ts +35 -0
  5. package/dist/agent/health-check.js +243 -0
  6. package/dist/agent/pty-runner.d.ts +1 -0
  7. package/dist/agent/pty-runner.js +6 -1
  8. package/dist/capture/transcript-repair.d.ts +1 -0
  9. package/dist/capture/transcript-repair.js +12 -1
  10. package/dist/commands/agent.d.ts +6 -0
  11. package/dist/commands/agent.js +244 -0
  12. package/dist/commands/dashboard.d.ts +25 -0
  13. package/dist/commands/dashboard.js +1175 -0
  14. package/dist/commands/run.d.ts +3 -0
  15. package/dist/commands/run.js +503 -350
  16. package/dist/commands/setup-remote.js +146 -37
  17. package/dist/commands/swarm-dashboard.d.ts +20 -0
  18. package/dist/commands/swarm-dashboard.js +735 -0
  19. package/dist/commands/swarm-setup.d.ts +10 -0
  20. package/dist/commands/swarm-setup.js +956 -0
  21. package/dist/commands/swarm.d.ts +46 -0
  22. package/dist/commands/swarm.js +441 -0
  23. package/dist/commands/test-claude.d.ts +16 -0
  24. package/dist/commands/test-claude.js +156 -0
  25. package/dist/commands/usage/blocks.d.ts +8 -0
  26. package/dist/commands/usage/blocks.js +60 -0
  27. package/dist/commands/usage/daily.d.ts +9 -0
  28. package/dist/commands/usage/daily.js +96 -0
  29. package/dist/commands/usage/dashboard.d.ts +8 -0
  30. package/dist/commands/usage/dashboard.js +104 -0
  31. package/dist/commands/usage/formatters.d.ts +41 -0
  32. package/dist/commands/usage/formatters.js +147 -0
  33. package/dist/commands/usage/index.d.ts +13 -0
  34. package/dist/commands/usage/index.js +87 -0
  35. package/dist/commands/usage/monthly.d.ts +8 -0
  36. package/dist/commands/usage/monthly.js +66 -0
  37. package/dist/commands/usage/session.d.ts +11 -0
  38. package/dist/commands/usage/session.js +193 -0
  39. package/dist/commands/usage/weekly.d.ts +9 -0
  40. package/dist/commands/usage/weekly.js +61 -0
  41. package/dist/deploy/instructions.d.ts +5 -2
  42. package/dist/deploy/instructions.js +11 -8
  43. package/dist/index.js +256 -20
  44. package/dist/lib/tmux-scrollbar.d.ts +14 -0
  45. package/dist/lib/tmux-scrollbar.js +296 -0
  46. package/dist/lib/usage-parser.d.ts +95 -5
  47. package/dist/lib/usage-parser.js +416 -71
  48. package/dist/utils/log-rotate.d.ts +18 -0
  49. package/dist/utils/log-rotate.js +74 -0
  50. package/dist/utils/platform.d.ts +2 -0
  51. package/dist/utils/platform.js +3 -1
  52. package/dist/utils/session-binding.d.ts +5 -0
  53. package/dist/utils/session-binding.js +46 -0
  54. package/dist/utils/state.js +4 -0
  55. package/dist/utils/verify-remote-terminal.d.ts +10 -0
  56. package/dist/utils/verify-remote-terminal.js +415 -0
  57. package/package.json +16 -11
  58. package/templates/CLAUDE.md +135 -23
  59. package/templates/cursor-hooks/after-agent-response.sh +0 -0
  60. package/templates/cursor-hooks/before-submit-prompt.sh +0 -0
  61. package/templates/cursor-hooks/stop.sh +0 -0
  62. package/templates/ekkos-manifest.json +5 -5
  63. package/templates/hooks/assistant-response.sh +0 -0
  64. package/templates/hooks/lib/contract.sh +43 -31
  65. package/templates/hooks/lib/count-tokens.cjs +86 -0
  66. package/templates/hooks/lib/ekkos-reminders.sh +98 -0
  67. package/templates/hooks/lib/state.sh +53 -1
  68. package/templates/hooks/session-start.sh +0 -0
  69. package/templates/hooks/stop.sh +150 -388
  70. package/templates/hooks/user-prompt-submit.sh +353 -443
  71. package/templates/plan-template.md +0 -0
  72. package/templates/spec-template.md +0 -0
  73. package/templates/windsurf-hooks/README.md +212 -0
  74. package/templates/windsurf-hooks/hooks.json +9 -2
  75. package/templates/windsurf-hooks/install.sh +148 -0
  76. package/templates/windsurf-hooks/lib/contract.sh +2 -0
  77. package/templates/windsurf-hooks/post-cascade-response.sh +251 -0
  78. package/templates/windsurf-hooks/pre-user-prompt.sh +435 -0
  79. package/templates/windsurf-skills/ekkos-memory/SKILL.md +219 -0
  80. package/LICENSE +0 -21
  81. package/templates/windsurf-hooks/before-submit-prompt.sh +0 -238
@@ -0,0 +1,243 @@
1
+ "use strict";
2
+ /**
3
+ * Health check for ekkOS agent daemon
4
+ *
5
+ * Verifies:
6
+ * - Service is installed and loaded
7
+ * - Process is running
8
+ * - Recent activity in logs
9
+ * - Network connectivity to relay server
10
+ */
11
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ var desc = Object.getOwnPropertyDescriptor(m, k);
14
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
15
+ desc = { enumerable: true, get: function() { return m[k]; } };
16
+ }
17
+ Object.defineProperty(o, k2, desc);
18
+ }) : (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ o[k2] = m[k];
21
+ }));
22
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
23
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
24
+ }) : function(o, v) {
25
+ o["default"] = v;
26
+ });
27
+ var __importStar = (this && this.__importStar) || (function () {
28
+ var ownKeys = function(o) {
29
+ ownKeys = Object.getOwnPropertyNames || function (o) {
30
+ var ar = [];
31
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
32
+ return ar;
33
+ };
34
+ return ownKeys(o);
35
+ };
36
+ return function (mod) {
37
+ if (mod && mod.__esModule) return mod;
38
+ var result = {};
39
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
40
+ __setModuleDefault(result, mod);
41
+ return result;
42
+ };
43
+ })();
44
+ var __importDefault = (this && this.__importDefault) || function (mod) {
45
+ return (mod && mod.__esModule) ? mod : { "default": mod };
46
+ };
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ exports.checkDaemonHealth = checkDaemonHealth;
49
+ exports.formatHealthStatus = formatHealthStatus;
50
+ const os = __importStar(require("os"));
51
+ const fs = __importStar(require("fs"));
52
+ const path = __importStar(require("path"));
53
+ const child_process_1 = require("child_process");
54
+ const ws_1 = __importDefault(require("ws"));
55
+ const RELAY_URL = process.env.RELAY_WS_URL || 'wss://ekkos-relay-production.up.railway.app';
56
+ /**
57
+ * Check agent daemon health
58
+ */
59
+ async function checkDaemonHealth() {
60
+ const status = {
61
+ ok: true,
62
+ service: {
63
+ installed: false,
64
+ loaded: false,
65
+ running: false,
66
+ },
67
+ logs: {
68
+ recentErrors: [],
69
+ },
70
+ relay: {
71
+ reachable: false,
72
+ },
73
+ };
74
+ // Check if service is installed
75
+ const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', 'dev.ekkos.agent.plist');
76
+ status.service.installed = fs.existsSync(plistPath);
77
+ // Check if service is loaded
78
+ try {
79
+ const output = (0, child_process_1.execSync)('launchctl list | grep dev.ekkos.agent', { encoding: 'utf-8' }).trim();
80
+ status.service.loaded = !!output;
81
+ // Extract PID if running
82
+ const pidMatch = output.match(/^(\d+)\s+/);
83
+ if (pidMatch) {
84
+ status.service.pid = parseInt(pidMatch[1], 10);
85
+ status.service.running = status.service.pid > 0;
86
+ }
87
+ }
88
+ catch {
89
+ // Service not loaded
90
+ status.service.loaded = false;
91
+ status.service.running = false;
92
+ }
93
+ // Check logs
94
+ const logDir = path.join(os.homedir(), '.ekkos');
95
+ const errLogPath = path.join(logDir, 'agent.err.log');
96
+ const outLogPath = path.join(logDir, 'agent.out.log');
97
+ if (fs.existsSync(errLogPath)) {
98
+ try {
99
+ const errLog = fs.readFileSync(errLogPath, 'utf-8');
100
+ if (errLog) {
101
+ status.logs.lastActivity = new Date(fs.statSync(errLogPath).mtime);
102
+ status.logs.recentErrors = extractRecentErrors(errLog, 10);
103
+ }
104
+ }
105
+ catch {
106
+ // Ignore log read errors
107
+ }
108
+ }
109
+ if (fs.existsSync(outLogPath)) {
110
+ try {
111
+ const stat = fs.statSync(outLogPath);
112
+ const mtime = new Date(stat.mtime);
113
+ if (!status.logs.lastActivity || mtime > status.logs.lastActivity) {
114
+ status.logs.lastActivity = mtime;
115
+ }
116
+ }
117
+ catch {
118
+ // Ignore
119
+ }
120
+ }
121
+ // Check relay connectivity
122
+ try {
123
+ status.relay.reachable = await checkRelayConnectivity();
124
+ }
125
+ catch (err) {
126
+ status.relay.reachable = false;
127
+ status.relay.lastError = err.message;
128
+ }
129
+ // Overall health
130
+ status.ok = status.service.running && status.relay.reachable && status.logs.recentErrors.length === 0;
131
+ return status;
132
+ }
133
+ /**
134
+ * Check if relay server is reachable
135
+ */
136
+ async function checkRelayConnectivity() {
137
+ return new Promise((resolve) => {
138
+ const timeout = setTimeout(() => {
139
+ ws.terminate();
140
+ resolve(false);
141
+ }, 5000);
142
+ const ws = new ws_1.default(`${RELAY_URL}/health`);
143
+ ws.on('open', () => {
144
+ clearTimeout(timeout);
145
+ ws.close();
146
+ resolve(true);
147
+ });
148
+ ws.on('error', () => {
149
+ clearTimeout(timeout);
150
+ resolve(false);
151
+ });
152
+ ws.on('close', () => {
153
+ clearTimeout(timeout);
154
+ resolve(false);
155
+ });
156
+ });
157
+ }
158
+ /**
159
+ * Extract recent error lines from log
160
+ */
161
+ function extractRecentErrors(log, count) {
162
+ return log
163
+ .split('\n')
164
+ .filter((line) => {
165
+ const lower = line.toLowerCase();
166
+ return lower.includes('error') || lower.includes('failed') || lower.includes('exception');
167
+ })
168
+ .slice(-count);
169
+ }
170
+ /**
171
+ * Format health status for console output
172
+ */
173
+ function formatHealthStatus(status) {
174
+ const lines = [];
175
+ lines.push('ekkOS Agent Daemon Health Check');
176
+ lines.push('================================\n');
177
+ // Service status
178
+ lines.push(`Service Installation: ${status.service.installed ? '✓' : '✗'} ${status.service.installed ? 'Installed' : 'Not installed'}`);
179
+ lines.push(`Service Loaded: ${status.service.loaded ? '✓' : '✗'} ${status.service.loaded ? 'Loaded' : 'Not loaded'}`);
180
+ if (status.service.running) {
181
+ lines.push(`Service Running: ✓ Running (PID ${status.service.pid})`);
182
+ }
183
+ else {
184
+ lines.push('Service Running: ✗ Not running');
185
+ }
186
+ lines.push('');
187
+ // Logs status
188
+ if (status.logs.lastActivity) {
189
+ const now = new Date();
190
+ const age = now.getTime() - status.logs.lastActivity.getTime();
191
+ const ageStr = formatAge(age);
192
+ lines.push(`Last Activity: ${ageStr} ago (${status.logs.lastActivity.toISOString()})`);
193
+ }
194
+ else {
195
+ lines.push('Last Activity: No logs found');
196
+ }
197
+ if (status.logs.recentErrors.length > 0) {
198
+ lines.push(`Recent Errors (${status.logs.recentErrors.length}):`);
199
+ for (const err of status.logs.recentErrors) {
200
+ lines.push(` - ${err.substring(0, 100)}`);
201
+ }
202
+ }
203
+ lines.push('');
204
+ // Relay status
205
+ lines.push(`Relay Server: ${status.relay.reachable ? '✓' : '✗'} ${status.relay.reachable ? 'Reachable' : 'Unreachable'}`);
206
+ if (status.relay.lastError) {
207
+ lines.push(` Error: ${status.relay.lastError}`);
208
+ }
209
+ lines.push('');
210
+ // Overall status
211
+ if (status.ok) {
212
+ lines.push('Overall: ✓ Healthy - All systems operational');
213
+ }
214
+ else {
215
+ lines.push('Overall: ✗ Unhealthy - Issues detected');
216
+ if (!status.service.running) {
217
+ lines.push(' → Service is not running. Run: launchctl start dev.ekkos.agent');
218
+ }
219
+ if (!status.relay.reachable) {
220
+ lines.push(' → Cannot reach relay server. Check network connectivity.');
221
+ }
222
+ if (status.logs.recentErrors.length > 0) {
223
+ lines.push(' → Recent errors found in logs. Check ~/.ekkos/agent.err.log');
224
+ }
225
+ }
226
+ return lines.join('\n');
227
+ }
228
+ /**
229
+ * Format age duration
230
+ */
231
+ function formatAge(ms) {
232
+ const seconds = Math.floor(ms / 1000);
233
+ const minutes = Math.floor(seconds / 60);
234
+ const hours = Math.floor(minutes / 60);
235
+ const days = Math.floor(hours / 24);
236
+ if (days > 0)
237
+ return `${days}d ${hours % 24}h`;
238
+ if (hours > 0)
239
+ return `${hours}h ${minutes % 60}m`;
240
+ if (minutes > 0)
241
+ return `${minutes}m ${seconds % 60}s`;
242
+ return `${seconds}s`;
243
+ }
@@ -12,6 +12,7 @@ interface PTYRunnerConfig {
12
12
  cols?: number;
13
13
  rows?: number;
14
14
  cwd?: string;
15
+ env?: NodeJS.ProcessEnv;
15
16
  verbose?: boolean;
16
17
  }
17
18
  export declare class PTYRunner {
@@ -85,6 +85,7 @@ class PTYRunner {
85
85
  cwd: this.config.cwd || process.cwd(),
86
86
  env: {
87
87
  ...process.env,
88
+ ...(this.config.env || {}),
88
89
  TERM: 'xterm-256color',
89
90
  COLORTERM: 'truecolor',
90
91
  },
@@ -107,7 +108,10 @@ class PTYRunner {
107
108
  this.spawnProcess = (0, child_process_1.spawn)('cmd.exe', ['/c', this.config.command, ...this.config.args], {
108
109
  stdio: ['pipe', 'pipe', 'pipe'],
109
110
  cwd,
110
- env: process.env,
111
+ env: {
112
+ ...process.env,
113
+ ...(this.config.env || {}),
114
+ },
111
115
  });
112
116
  }
113
117
  else {
@@ -120,6 +124,7 @@ class PTYRunner {
120
124
  cwd,
121
125
  env: {
122
126
  ...process.env,
127
+ ...(this.config.env || {}),
123
128
  TERM: 'xterm-256color',
124
129
  },
125
130
  });
@@ -24,6 +24,7 @@ export interface RepairResult {
24
24
  export declare function countOrphansInJsonl(jsonlPath: string): {
25
25
  orphans: number;
26
26
  orphanIds: string[];
27
+ lineCount: number;
27
28
  };
28
29
  /**
29
30
  * Main entry point: repair or rollback a transcript with orphan tool_results.
@@ -98,7 +98,7 @@ function countOrphansInJsonl(jsonlPath) {
98
98
  }
99
99
  }
100
100
  }
101
- return { orphans: orphanIds.length, orphanIds };
101
+ return { orphans: orphanIds.length, orphanIds, lineCount: lines.length };
102
102
  }
103
103
  /**
104
104
  * Find plausible backups for a jsonl file.
@@ -258,6 +258,17 @@ function repairOrRollbackTranscript(jsonlPath) {
258
258
  orphans: v.orphans,
259
259
  });
260
260
  if (v.orphans === 0) {
261
+ // SAFETY CHECK: Prevent session amnesia (rollback > 20 lines)
262
+ const lostLines = (initial.lineCount || 0) - (v.lineCount || 0);
263
+ if (lostLines > 20) {
264
+ debugLog('REPAIR_BACKUP_SKIPPED', 'Backup too old - skipping to prevent data loss', {
265
+ backup,
266
+ currentLines: initial.lineCount,
267
+ backupLines: v.lineCount,
268
+ lostLines
269
+ });
270
+ continue;
271
+ }
261
272
  atomicReplace(jsonlPath, backup);
262
273
  debugLog('REPAIR_ROLLBACK_SUCCESS', 'Rolled back to valid backup', { backup });
263
274
  return { action: 'rollback', orphansFound: initial.orphans, backupUsed: backup };
@@ -42,3 +42,9 @@ export declare function agentUninstall(options?: AgentOptions): Promise<void>;
42
42
  export declare function agentLogs(options?: {
43
43
  follow?: boolean;
44
44
  }): Promise<void>;
45
+ /**
46
+ * Check agent daemon health and diagnose connection issues
47
+ */
48
+ export declare function agentHealth(options?: {
49
+ json?: boolean;
50
+ }): Promise<void>;
@@ -54,6 +54,7 @@ exports.agentRestart = agentRestart;
54
54
  exports.agentStatus = agentStatus;
55
55
  exports.agentUninstall = agentUninstall;
56
56
  exports.agentLogs = agentLogs;
57
+ exports.agentHealth = agentHealth;
57
58
  const chalk_1 = __importDefault(require("chalk"));
58
59
  const os = __importStar(require("os"));
59
60
  const fs = __importStar(require("fs"));
@@ -298,3 +299,246 @@ async function agentLogs(options = {}) {
298
299
  console.log(lines.join('\n'));
299
300
  }
300
301
  }
302
+ /**
303
+ * Check agent daemon health and diagnose connection issues
304
+ */
305
+ async function agentHealth(options = {}) {
306
+ const platform = os.platform();
307
+ const deviceFilePath = path.join(state_1.EKKOS_DIR, 'device.json');
308
+ const logPath = path.join(state_1.EKKOS_DIR, 'agent.out.log');
309
+ const pidFilePath = path.join(state_1.EKKOS_DIR, 'agent.pid');
310
+ const health = {
311
+ timestamp: new Date().toISOString(),
312
+ status: 'unknown',
313
+ checks: {
314
+ configured: false,
315
+ paired: false,
316
+ serviceRunning: false,
317
+ processRunning: false,
318
+ logsWriting: false,
319
+ cloudConnected: false,
320
+ networkHealthy: false,
321
+ },
322
+ issues: [],
323
+ suggestions: [],
324
+ };
325
+ // Check 1: Device configured
326
+ if (fs.existsSync(deviceFilePath)) {
327
+ const deviceData = JSON.parse(fs.readFileSync(deviceFilePath, 'utf-8'));
328
+ health.checks.configured = true;
329
+ health.device = {
330
+ name: deviceData.deviceName,
331
+ id: deviceData.deviceId.slice(0, 8) + '...',
332
+ platform: deviceData.platform,
333
+ arch: deviceData.arch,
334
+ };
335
+ // Check 2: Device paired
336
+ if (deviceData.deviceToken) {
337
+ health.checks.paired = true;
338
+ health.pairedAt = deviceData.pairedAt;
339
+ }
340
+ else {
341
+ health.checks.paired = false;
342
+ health.issues.push('Device not paired');
343
+ health.suggestions.push('Run `ekkos setup-remote` to pair device');
344
+ }
345
+ // Check 3: Service running
346
+ try {
347
+ if (platform === 'darwin') {
348
+ const output = (0, child_process_1.execSync)('launchctl list | grep dev.ekkos.agent || true', { encoding: 'utf-8' });
349
+ health.checks.serviceRunning = output.includes('dev.ekkos.agent');
350
+ }
351
+ else if (platform === 'win32') {
352
+ const output = (0, child_process_1.execSync)('schtasks /query /tn "ekkOS Agent" 2>nul || true', { encoding: 'utf-8' });
353
+ health.checks.serviceRunning = output.includes('Running');
354
+ }
355
+ else if (platform === 'linux') {
356
+ const output = (0, child_process_1.execSync)('systemctl --user is-active ekkos-agent 2>/dev/null || true', { encoding: 'utf-8' });
357
+ health.checks.serviceRunning = output.trim() === 'active';
358
+ }
359
+ }
360
+ catch {
361
+ health.checks.serviceRunning = false;
362
+ }
363
+ if (!health.checks.serviceRunning) {
364
+ health.issues.push('Service not running');
365
+ health.suggestions.push('Run `ekkos agent start` to start the service');
366
+ }
367
+ // Check 4: Process running
368
+ if (fs.existsSync(pidFilePath)) {
369
+ const pid = parseInt(fs.readFileSync(pidFilePath, 'utf-8').trim());
370
+ try {
371
+ // Check if process exists
372
+ (0, child_process_1.execSync)(`kill -0 ${pid} 2>/dev/null`, { stdio: 'pipe' });
373
+ health.checks.processRunning = true;
374
+ }
375
+ catch {
376
+ health.checks.processRunning = false;
377
+ health.issues.push(`Agent process (PID ${pid}) not running`);
378
+ health.suggestions.push('Run `ekkos agent restart` to restart');
379
+ }
380
+ }
381
+ else {
382
+ health.checks.processRunning = false;
383
+ health.issues.push('No PID file found');
384
+ }
385
+ // Check 5: Logs writing
386
+ if (fs.existsSync(logPath)) {
387
+ const stats = fs.statSync(logPath);
388
+ const lastModified = Date.now() - stats.mtime.getTime();
389
+ const fifteenMinutes = 15 * 60 * 1000;
390
+ health.checks.logsWriting = lastModified < fifteenMinutes;
391
+ health.lastLogWrite = {
392
+ timestamp: stats.mtime.toISOString(),
393
+ minutesAgo: Math.round(lastModified / 1000 / 60),
394
+ };
395
+ if (!health.checks.logsWriting) {
396
+ health.issues.push(`No logs written for ${Math.round(lastModified / 1000 / 60)} minutes`);
397
+ health.suggestions.push('Check if daemon crashed - run `ekkos agent logs -f` to monitor');
398
+ }
399
+ // Parse recent errors from logs
400
+ const content = fs.readFileSync(logPath, 'utf-8');
401
+ const lines = content.split('\n').slice(-100);
402
+ const errors = lines.filter(l => l.includes('ERROR') || l.includes('error') || l.includes('Connection failed'));
403
+ if (errors.length > 0) {
404
+ health.recentErrors = errors.slice(-5);
405
+ }
406
+ }
407
+ else {
408
+ health.checks.logsWriting = false;
409
+ health.issues.push('Log file not found');
410
+ }
411
+ // Check 6: Cloud connectivity
412
+ if (health.checks.paired) {
413
+ try {
414
+ const authToken = (0, state_1.getAuthToken)();
415
+ const state = (0, state_1.getState)();
416
+ if (authToken && state?.userId) {
417
+ const MEMORY_API_URL = process.env.MEMORY_API_URL || 'https://api.ekkos.dev';
418
+ const controller = new AbortController();
419
+ const timeout = setTimeout(() => controller.abort(), 5000);
420
+ try {
421
+ const response = await fetch(`${MEMORY_API_URL}/api/v1/relay/devices/${state.userId}`, {
422
+ headers: { 'Authorization': `Bearer ${authToken}` },
423
+ signal: controller.signal,
424
+ });
425
+ clearTimeout(timeout);
426
+ if (response.ok) {
427
+ const data = await response.json();
428
+ const device = data.devices?.find((d) => d.deviceId === deviceData.deviceId);
429
+ health.checks.cloudConnected = device?.online === true;
430
+ if (!health.checks.cloudConnected) {
431
+ health.issues.push('Device shows offline in cloud');
432
+ health.suggestions.push('Check network connection and agent logs');
433
+ }
434
+ }
435
+ else {
436
+ health.checks.cloudConnected = false;
437
+ health.issues.push(`Cloud API returned ${response.status}`);
438
+ }
439
+ }
440
+ catch (err) {
441
+ clearTimeout(timeout);
442
+ health.checks.cloudConnected = false;
443
+ if (err.name === 'AbortError') {
444
+ health.issues.push('Cloud connection timeout (>5s)');
445
+ }
446
+ else {
447
+ health.issues.push(`Cloud connection error: ${err.message}`);
448
+ }
449
+ health.suggestions.push('Check internet connection');
450
+ }
451
+ }
452
+ else {
453
+ health.issues.push('Not logged in to ekkOS');
454
+ health.suggestions.push('Run `ekkos init` to set up authentication');
455
+ }
456
+ }
457
+ catch {
458
+ health.checks.cloudConnected = false;
459
+ health.issues.push('Unable to check cloud connectivity');
460
+ }
461
+ }
462
+ // Check 7: Network health
463
+ if (health.checks.logsWriting && health.checks.cloudConnected) {
464
+ health.checks.networkHealthy = true;
465
+ }
466
+ }
467
+ else {
468
+ health.checks.configured = false;
469
+ health.issues.push('Device not configured');
470
+ health.suggestions.push('Run `ekkos setup-remote` to configure device');
471
+ }
472
+ // Determine overall status
473
+ if (health.issues.length === 0) {
474
+ health.status = 'healthy';
475
+ }
476
+ else if (health.checks.serviceRunning &&
477
+ health.checks.processRunning &&
478
+ health.checks.logsWriting) {
479
+ health.status = 'degraded';
480
+ }
481
+ else {
482
+ health.status = 'unhealthy';
483
+ }
484
+ if (options.json) {
485
+ console.log(JSON.stringify(health, null, 2));
486
+ }
487
+ else {
488
+ // Pretty print
489
+ console.log('');
490
+ console.log(chalk_1.default.cyan.bold(' ekkOS Agent Health Check'));
491
+ console.log('');
492
+ const statusColor = health.status === 'healthy' ? chalk_1.default.green :
493
+ health.status === 'degraded' ? chalk_1.default.yellow :
494
+ chalk_1.default.red;
495
+ console.log(` Status: ${statusColor.bold(health.status.toUpperCase())}`);
496
+ console.log('');
497
+ // Device info
498
+ if (health.device) {
499
+ console.log(chalk_1.default.gray(' Device:'));
500
+ console.log(` ${chalk_1.default.gray('Name:')} ${health.device.name}`);
501
+ console.log(` ${chalk_1.default.gray('ID:')} ${health.device.id}`);
502
+ console.log(` ${chalk_1.default.gray('Platform:')} ${health.device.platform}/${health.device.arch}`);
503
+ console.log('');
504
+ }
505
+ // Checks
506
+ console.log(chalk_1.default.gray(' Checks:'));
507
+ for (const [key, value] of Object.entries(health.checks)) {
508
+ const icon = value ? chalk_1.default.green('✓') : chalk_1.default.red('✗');
509
+ const label = key.replace(/([A-Z])/g, ' $1').trim();
510
+ console.log(` ${icon} ${label}`);
511
+ }
512
+ console.log('');
513
+ // Issues
514
+ if (health.issues.length > 0) {
515
+ console.log(chalk_1.default.yellow(' Issues:'));
516
+ for (const issue of health.issues) {
517
+ console.log(` ${chalk_1.default.yellow('⚠')} ${issue}`);
518
+ }
519
+ console.log('');
520
+ }
521
+ // Suggestions
522
+ if (health.suggestions.length > 0) {
523
+ console.log(chalk_1.default.cyan(' Suggestions:'));
524
+ for (const suggestion of health.suggestions) {
525
+ console.log(` ${chalk_1.default.cyan('→')} ${suggestion}`);
526
+ }
527
+ console.log('');
528
+ }
529
+ // Recent errors
530
+ if (health.recentErrors && health.recentErrors.length > 0) {
531
+ console.log(chalk_1.default.red(' Recent Errors:'));
532
+ for (const err of health.recentErrors.slice(0, 3)) {
533
+ const preview = err.substring(0, 100) + (err.length > 100 ? '...' : '');
534
+ console.log(` ${chalk_1.default.red('!')} ${preview}`);
535
+ }
536
+ console.log('');
537
+ }
538
+ // Logs info
539
+ if (health.lastLogWrite) {
540
+ console.log(chalk_1.default.gray(` Last activity: ${health.lastLogWrite.minutesAgo} minutes ago`));
541
+ console.log('');
542
+ }
543
+ }
544
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * ekkos dashboard [session-name]
3
+ *
4
+ * Live TUI dashboard for monitoring Claude Code session usage in real-time.
5
+ * Uses blessed-contrib for rich terminal widgets (gauges, charts, tables).
6
+ *
7
+ * Usage:
8
+ * ekkos dashboard <session-name> Watch specific session
9
+ * ekkos dashboard --latest Auto-detect latest active session
10
+ * ekkos dashboard --wait-for-new Wait for a brand-new session to appear
11
+ * ekkos dashboard Interactive session picker
12
+ *
13
+ * Text Selection:
14
+ * To select text separately from Claude Code, run the dashboard in a different
15
+ * terminal window/pane. This prevents the blessed screen from interfering with
16
+ * Claude Code's text selection. Use iTerm2 split panes or tmux windows.
17
+ *
18
+ * Scrolling:
19
+ * Up/Down arrows or j/k Scroll one line
20
+ * PageUp/PageDown or u/d Scroll one page
21
+ * Home/End or g/G Jump to top/bottom
22
+ * Mouse wheel Scroll with mouse
23
+ */
24
+ import { Command } from 'commander';
25
+ export declare const dashboardCommand: Command;