@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.
- package/README.md +57 -0
- package/dist/agent/daemon.d.ts +27 -0
- package/dist/agent/daemon.js +254 -29
- package/dist/agent/health-check.d.ts +35 -0
- package/dist/agent/health-check.js +243 -0
- package/dist/agent/pty-runner.d.ts +1 -0
- package/dist/agent/pty-runner.js +6 -1
- package/dist/capture/transcript-repair.d.ts +1 -0
- package/dist/capture/transcript-repair.js +12 -1
- package/dist/commands/agent.d.ts +6 -0
- package/dist/commands/agent.js +244 -0
- package/dist/commands/dashboard.d.ts +25 -0
- package/dist/commands/dashboard.js +1175 -0
- package/dist/commands/run.d.ts +3 -0
- package/dist/commands/run.js +503 -350
- package/dist/commands/setup-remote.js +146 -37
- package/dist/commands/swarm-dashboard.d.ts +20 -0
- package/dist/commands/swarm-dashboard.js +735 -0
- package/dist/commands/swarm-setup.d.ts +10 -0
- package/dist/commands/swarm-setup.js +956 -0
- package/dist/commands/swarm.d.ts +46 -0
- package/dist/commands/swarm.js +441 -0
- package/dist/commands/test-claude.d.ts +16 -0
- package/dist/commands/test-claude.js +156 -0
- package/dist/commands/usage/blocks.d.ts +8 -0
- package/dist/commands/usage/blocks.js +60 -0
- package/dist/commands/usage/daily.d.ts +9 -0
- package/dist/commands/usage/daily.js +96 -0
- package/dist/commands/usage/dashboard.d.ts +8 -0
- package/dist/commands/usage/dashboard.js +104 -0
- package/dist/commands/usage/formatters.d.ts +41 -0
- package/dist/commands/usage/formatters.js +147 -0
- package/dist/commands/usage/index.d.ts +13 -0
- package/dist/commands/usage/index.js +87 -0
- package/dist/commands/usage/monthly.d.ts +8 -0
- package/dist/commands/usage/monthly.js +66 -0
- package/dist/commands/usage/session.d.ts +11 -0
- package/dist/commands/usage/session.js +193 -0
- package/dist/commands/usage/weekly.d.ts +9 -0
- package/dist/commands/usage/weekly.js +61 -0
- package/dist/deploy/instructions.d.ts +5 -2
- package/dist/deploy/instructions.js +11 -8
- package/dist/index.js +256 -20
- package/dist/lib/tmux-scrollbar.d.ts +14 -0
- package/dist/lib/tmux-scrollbar.js +296 -0
- package/dist/lib/usage-parser.d.ts +95 -5
- package/dist/lib/usage-parser.js +416 -71
- package/dist/utils/log-rotate.d.ts +18 -0
- package/dist/utils/log-rotate.js +74 -0
- package/dist/utils/platform.d.ts +2 -0
- package/dist/utils/platform.js +3 -1
- package/dist/utils/session-binding.d.ts +5 -0
- package/dist/utils/session-binding.js +46 -0
- package/dist/utils/state.js +4 -0
- package/dist/utils/verify-remote-terminal.d.ts +10 -0
- package/dist/utils/verify-remote-terminal.js +415 -0
- package/package.json +16 -11
- package/templates/CLAUDE.md +135 -23
- package/templates/cursor-hooks/after-agent-response.sh +0 -0
- package/templates/cursor-hooks/before-submit-prompt.sh +0 -0
- package/templates/cursor-hooks/stop.sh +0 -0
- package/templates/ekkos-manifest.json +5 -5
- package/templates/hooks/assistant-response.sh +0 -0
- package/templates/hooks/lib/contract.sh +43 -31
- package/templates/hooks/lib/count-tokens.cjs +86 -0
- package/templates/hooks/lib/ekkos-reminders.sh +98 -0
- package/templates/hooks/lib/state.sh +53 -1
- package/templates/hooks/session-start.sh +0 -0
- package/templates/hooks/stop.sh +150 -388
- package/templates/hooks/user-prompt-submit.sh +353 -443
- package/templates/plan-template.md +0 -0
- package/templates/spec-template.md +0 -0
- package/templates/windsurf-hooks/README.md +212 -0
- package/templates/windsurf-hooks/hooks.json +9 -2
- package/templates/windsurf-hooks/install.sh +148 -0
- package/templates/windsurf-hooks/lib/contract.sh +2 -0
- package/templates/windsurf-hooks/post-cascade-response.sh +251 -0
- package/templates/windsurf-hooks/pre-user-prompt.sh +435 -0
- package/templates/windsurf-skills/ekkos-memory/SKILL.md +219 -0
- package/LICENSE +0 -21
- 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
|
+
}
|
package/dist/agent/pty-runner.js
CHANGED
|
@@ -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:
|
|
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
|
});
|
|
@@ -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 };
|
package/dist/commands/agent.d.ts
CHANGED
|
@@ -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>;
|
package/dist/commands/agent.js
CHANGED
|
@@ -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;
|