@ekkos/cli 0.2.18 → 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 (98) 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/eviction-client.d.ts +139 -0
  9. package/dist/capture/eviction-client.js +454 -0
  10. package/dist/capture/index.d.ts +2 -0
  11. package/dist/capture/index.js +2 -0
  12. package/dist/capture/jsonl-rewriter.d.ts +96 -0
  13. package/dist/capture/jsonl-rewriter.js +1369 -0
  14. package/dist/capture/transcript-repair.d.ts +51 -0
  15. package/dist/capture/transcript-repair.js +319 -0
  16. package/dist/commands/agent.d.ts +6 -0
  17. package/dist/commands/agent.js +244 -0
  18. package/dist/commands/dashboard.d.ts +25 -0
  19. package/dist/commands/dashboard.js +1175 -0
  20. package/dist/commands/doctor.js +23 -1
  21. package/dist/commands/run.d.ts +5 -0
  22. package/dist/commands/run.js +1605 -516
  23. package/dist/commands/setup-remote.js +146 -37
  24. package/dist/commands/swarm-dashboard.d.ts +20 -0
  25. package/dist/commands/swarm-dashboard.js +735 -0
  26. package/dist/commands/swarm-setup.d.ts +10 -0
  27. package/dist/commands/swarm-setup.js +956 -0
  28. package/dist/commands/swarm.d.ts +46 -0
  29. package/dist/commands/swarm.js +441 -0
  30. package/dist/commands/test-claude.d.ts +16 -0
  31. package/dist/commands/test-claude.js +156 -0
  32. package/dist/commands/usage/blocks.d.ts +8 -0
  33. package/dist/commands/usage/blocks.js +60 -0
  34. package/dist/commands/usage/daily.d.ts +9 -0
  35. package/dist/commands/usage/daily.js +96 -0
  36. package/dist/commands/usage/dashboard.d.ts +8 -0
  37. package/dist/commands/usage/dashboard.js +104 -0
  38. package/dist/commands/usage/formatters.d.ts +41 -0
  39. package/dist/commands/usage/formatters.js +147 -0
  40. package/dist/commands/usage/index.d.ts +13 -0
  41. package/dist/commands/usage/index.js +87 -0
  42. package/dist/commands/usage/monthly.d.ts +8 -0
  43. package/dist/commands/usage/monthly.js +66 -0
  44. package/dist/commands/usage/session.d.ts +11 -0
  45. package/dist/commands/usage/session.js +193 -0
  46. package/dist/commands/usage/weekly.d.ts +9 -0
  47. package/dist/commands/usage/weekly.js +61 -0
  48. package/dist/commands/usage.d.ts +7 -0
  49. package/dist/commands/usage.js +214 -0
  50. package/dist/cron/index.d.ts +7 -0
  51. package/dist/cron/index.js +13 -0
  52. package/dist/cron/promoter.d.ts +70 -0
  53. package/dist/cron/promoter.js +403 -0
  54. package/dist/deploy/instructions.d.ts +5 -2
  55. package/dist/deploy/instructions.js +11 -8
  56. package/dist/index.js +262 -5
  57. package/dist/lib/tmux-scrollbar.d.ts +14 -0
  58. package/dist/lib/tmux-scrollbar.js +296 -0
  59. package/dist/lib/usage-monitor.d.ts +47 -0
  60. package/dist/lib/usage-monitor.js +124 -0
  61. package/dist/lib/usage-parser.d.ts +162 -0
  62. package/dist/lib/usage-parser.js +583 -0
  63. package/dist/restore/RestoreOrchestrator.d.ts +4 -0
  64. package/dist/restore/RestoreOrchestrator.js +118 -30
  65. package/dist/utils/log-rotate.d.ts +18 -0
  66. package/dist/utils/log-rotate.js +74 -0
  67. package/dist/utils/platform.d.ts +2 -0
  68. package/dist/utils/platform.js +3 -1
  69. package/dist/utils/session-binding.d.ts +5 -0
  70. package/dist/utils/session-binding.js +46 -0
  71. package/dist/utils/state.js +4 -0
  72. package/dist/utils/verify-remote-terminal.d.ts +10 -0
  73. package/dist/utils/verify-remote-terminal.js +415 -0
  74. package/package.json +9 -2
  75. package/templates/CLAUDE.md +135 -23
  76. package/templates/ekkos-manifest.json +5 -5
  77. package/templates/hooks/lib/contract.sh +43 -31
  78. package/templates/hooks/lib/count-tokens.cjs +86 -0
  79. package/templates/hooks/lib/ekkos-reminders.sh +98 -0
  80. package/templates/hooks/lib/state.sh +53 -1
  81. package/templates/hooks/stop.sh +150 -388
  82. package/templates/hooks/user-prompt-submit.sh +353 -443
  83. package/templates/windsurf-hooks/README.md +212 -0
  84. package/templates/windsurf-hooks/hooks.json +9 -2
  85. package/templates/windsurf-hooks/install.sh +148 -0
  86. package/templates/windsurf-hooks/lib/contract.sh +2 -0
  87. package/templates/windsurf-hooks/post-cascade-response.sh +251 -0
  88. package/templates/windsurf-hooks/pre-user-prompt.sh +435 -0
  89. package/templates/windsurf-skills/ekkos-memory/SKILL.md +219 -0
  90. package/templates/agents/README.md +0 -182
  91. package/templates/agents/code-reviewer.md +0 -166
  92. package/templates/agents/debug-detective.md +0 -169
  93. package/templates/agents/ekkOS_Vercel.md +0 -99
  94. package/templates/agents/extension-manager.md +0 -229
  95. package/templates/agents/git-companion.md +0 -185
  96. package/templates/agents/github-test-agent.md +0 -321
  97. package/templates/agents/railway-manager.md +0 -215
  98. package/templates/windsurf-hooks/before-submit-prompt.sh +0 -238
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Transcript Repair - Handles orphan tool_result recovery
3
+ *
4
+ * When ccDNA's validate mode detects orphan tool_results (tool_result without
5
+ * matching tool_use), this module repairs the transcript:
6
+ *
7
+ * 1. ROLLBACK (preferred): Restore from backup if valid
8
+ * 2. SURGICAL REPAIR (fallback): Remove orphan tool_result lines
9
+ *
10
+ * This closes the loop: ccDNA detects → ekkos-cli repairs → /clear + /continue
11
+ */
12
+ type RepairAction = 'none' | 'rollback' | 'surgical_repair' | 'failed';
13
+ export interface RepairResult {
14
+ action: RepairAction;
15
+ orphansFound: number;
16
+ removedLines?: number;
17
+ backupUsed?: string;
18
+ reason?: string;
19
+ }
20
+ /**
21
+ * Minimal validator: tool_result.tool_use_id must reference a tool_use.id that
22
+ * appears earlier in the transcript (in-order).
23
+ */
24
+ export declare function countOrphansInJsonl(jsonlPath: string): {
25
+ orphans: number;
26
+ orphanIds: string[];
27
+ lineCount: number;
28
+ };
29
+ /**
30
+ * Main entry point: repair or rollback a transcript with orphan tool_results.
31
+ *
32
+ * Strategy:
33
+ * 1. Check if there are orphans (exit early if none)
34
+ * 2. Try rollback to newest valid backup
35
+ * 3. If no valid backup, surgically remove orphan lines
36
+ */
37
+ export declare function repairOrRollbackTranscript(jsonlPath: string): RepairResult;
38
+ /**
39
+ * Quick validation check - can be called before operations to verify health.
40
+ */
41
+ export declare function isTranscriptHealthy(jsonlPath: string): boolean;
42
+ /**
43
+ * Get detailed transcript health info.
44
+ */
45
+ export declare function getTranscriptHealth(jsonlPath: string): {
46
+ exists: boolean;
47
+ healthy: boolean;
48
+ orphans: number;
49
+ orphanIds: string[];
50
+ };
51
+ export {};
@@ -0,0 +1,319 @@
1
+ "use strict";
2
+ /**
3
+ * Transcript Repair - Handles orphan tool_result recovery
4
+ *
5
+ * When ccDNA's validate mode detects orphan tool_results (tool_result without
6
+ * matching tool_use), this module repairs the transcript:
7
+ *
8
+ * 1. ROLLBACK (preferred): Restore from backup if valid
9
+ * 2. SURGICAL REPAIR (fallback): Remove orphan tool_result lines
10
+ *
11
+ * This closes the loop: ccDNA detects → ekkos-cli repairs → /clear + /continue
12
+ */
13
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ var desc = Object.getOwnPropertyDescriptor(m, k);
16
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
17
+ desc = { enumerable: true, get: function() { return m[k]; } };
18
+ }
19
+ Object.defineProperty(o, k2, desc);
20
+ }) : (function(o, m, k, k2) {
21
+ if (k2 === undefined) k2 = k;
22
+ o[k2] = m[k];
23
+ }));
24
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
25
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
26
+ }) : function(o, v) {
27
+ o["default"] = v;
28
+ });
29
+ var __importStar = (this && this.__importStar) || (function () {
30
+ var ownKeys = function(o) {
31
+ ownKeys = Object.getOwnPropertyNames || function (o) {
32
+ var ar = [];
33
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
34
+ return ar;
35
+ };
36
+ return ownKeys(o);
37
+ };
38
+ return function (mod) {
39
+ if (mod && mod.__esModule) return mod;
40
+ var result = {};
41
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
42
+ __setModuleDefault(result, mod);
43
+ return result;
44
+ };
45
+ })();
46
+ Object.defineProperty(exports, "__esModule", { value: true });
47
+ exports.countOrphansInJsonl = countOrphansInJsonl;
48
+ exports.repairOrRollbackTranscript = repairOrRollbackTranscript;
49
+ exports.isTranscriptHealthy = isTranscriptHealthy;
50
+ exports.getTranscriptHealth = getTranscriptHealth;
51
+ const fs = __importStar(require("fs"));
52
+ const path = __importStar(require("path"));
53
+ const os = __importStar(require("os"));
54
+ // Debug logging to the same file as eviction
55
+ function debugLog(category, msg, data) {
56
+ try {
57
+ const logDir = path.join(os.homedir(), '.ekkos', 'logs');
58
+ if (!fs.existsSync(logDir))
59
+ fs.mkdirSync(logDir, { recursive: true });
60
+ const logPath = path.join(logDir, 'eviction-debug.log');
61
+ const ts = new Date().toISOString();
62
+ const line = `[${ts}] [REPAIR:${category}] ${msg}${data ? '\n ' + JSON.stringify(data, null, 2).replace(/\n/g, '\n ') : ''}`;
63
+ fs.appendFileSync(logPath, line + '\n');
64
+ }
65
+ catch {
66
+ // Silent fail
67
+ }
68
+ }
69
+ /**
70
+ * Minimal validator: tool_result.tool_use_id must reference a tool_use.id that
71
+ * appears earlier in the transcript (in-order).
72
+ */
73
+ function countOrphansInJsonl(jsonlPath) {
74
+ const content = fs.readFileSync(jsonlPath, 'utf-8');
75
+ const lines = content.split('\n').filter(l => l.trim().length > 0);
76
+ const seenToolUses = new Set();
77
+ const orphanIds = [];
78
+ for (const line of lines) {
79
+ let obj;
80
+ try {
81
+ obj = JSON.parse(line);
82
+ }
83
+ catch {
84
+ continue;
85
+ }
86
+ const blocks = obj?.message?.content;
87
+ if (!Array.isArray(blocks))
88
+ continue;
89
+ for (const b of blocks) {
90
+ const block = b;
91
+ if (block?.type === 'tool_use' && typeof block?.id === 'string') {
92
+ seenToolUses.add(block.id);
93
+ }
94
+ else if (block?.type === 'tool_result' && typeof block?.tool_use_id === 'string') {
95
+ if (!seenToolUses.has(block.tool_use_id)) {
96
+ orphanIds.push(block.tool_use_id);
97
+ }
98
+ }
99
+ }
100
+ }
101
+ return { orphans: orphanIds.length, orphanIds, lineCount: lines.length };
102
+ }
103
+ /**
104
+ * Find plausible backups for a jsonl file.
105
+ * Our jsonl-rewriter uses: ${filePath}.backup (single backup)
106
+ */
107
+ function findBackups(jsonlPath) {
108
+ const dir = path.dirname(jsonlPath);
109
+ const base = path.basename(jsonlPath);
110
+ if (!fs.existsSync(dir))
111
+ return [];
112
+ const candidates = [];
113
+ // Primary backup: exact .backup suffix (from jsonl-rewriter.ts)
114
+ const primaryBackup = `${jsonlPath}.backup`;
115
+ if (fs.existsSync(primaryBackup)) {
116
+ candidates.push(primaryBackup);
117
+ }
118
+ // Also check for other backup patterns in case user has manual backups
119
+ const files = fs.readdirSync(dir);
120
+ const additionalBackups = files
121
+ .filter(f => f.startsWith(base) &&
122
+ f !== base &&
123
+ (f.includes('.bak') || f.includes('.backup') || f.includes('.old') || f.includes('.prev')))
124
+ .map(f => path.join(dir, f))
125
+ .filter(f => !candidates.includes(f)) // Don't duplicate
126
+ .sort((a, b) => {
127
+ // Sort by mtime, newest first
128
+ const am = fs.statSync(a).mtimeMs;
129
+ const bm = fs.statSync(b).mtimeMs;
130
+ return bm - am;
131
+ });
132
+ return [...candidates, ...additionalBackups];
133
+ }
134
+ /**
135
+ * Atomically replace target file with source file.
136
+ * Keeps the corrupted version for forensics.
137
+ */
138
+ function atomicReplace(target, source) {
139
+ const dir = path.dirname(target);
140
+ const tmp = path.join(dir, `${path.basename(target)}.tmp-repair-${Date.now()}`);
141
+ fs.copyFileSync(source, tmp);
142
+ const corrupt = path.join(dir, `${path.basename(target)}.corrupt-${Date.now()}`);
143
+ fs.renameSync(target, corrupt);
144
+ fs.renameSync(tmp, target);
145
+ debugLog('ATOMIC_REPLACE', 'Replaced corrupt file', {
146
+ target,
147
+ source,
148
+ corruptBackup: corrupt,
149
+ });
150
+ }
151
+ /**
152
+ * Surgical repair: drop any line that contains a tool_result whose tool_use_id
153
+ * has not appeared earlier in the file. This unbricks at the cost of losing those results.
154
+ */
155
+ function surgicalRepair(jsonlPath) {
156
+ const content = fs.readFileSync(jsonlPath, 'utf-8');
157
+ const lines = content.split('\n').filter(l => l.trim().length > 0);
158
+ const seenToolUses = new Set();
159
+ const kept = [];
160
+ let removed = 0;
161
+ for (const line of lines) {
162
+ let obj;
163
+ try {
164
+ obj = JSON.parse(line);
165
+ }
166
+ catch {
167
+ // Keep non-JSON lines (shouldn't happen, but safe)
168
+ kept.push(line);
169
+ continue;
170
+ }
171
+ const blocks = obj?.message?.content;
172
+ if (!Array.isArray(blocks)) {
173
+ kept.push(line);
174
+ continue;
175
+ }
176
+ // First pass: collect tool_use IDs from this line
177
+ const lineToolUseIds = [];
178
+ for (const b of blocks) {
179
+ const block = b;
180
+ if (block?.type === 'tool_use' && typeof block?.id === 'string') {
181
+ lineToolUseIds.push(block.id);
182
+ }
183
+ }
184
+ // Second pass: check for orphan tool_results
185
+ let hasOrphanResult = false;
186
+ for (const b of blocks) {
187
+ const block = b;
188
+ if (block?.type === 'tool_result' && typeof block?.tool_use_id === 'string') {
189
+ if (!seenToolUses.has(block.tool_use_id)) {
190
+ hasOrphanResult = true;
191
+ debugLog('SURGICAL_DROP', 'Dropping line with orphan tool_result', {
192
+ tool_use_id: block.tool_use_id,
193
+ linePreview: line.slice(0, 200),
194
+ });
195
+ }
196
+ }
197
+ }
198
+ if (hasOrphanResult) {
199
+ removed++;
200
+ continue;
201
+ }
202
+ // If kept, update seen tool uses AFTER keeping (order matters)
203
+ for (const id of lineToolUseIds) {
204
+ seenToolUses.add(id);
205
+ }
206
+ kept.push(line);
207
+ }
208
+ // Write atomically
209
+ const dir = path.dirname(jsonlPath);
210
+ const tmp = path.join(dir, `${path.basename(jsonlPath)}.tmp-surgical-${Date.now()}`);
211
+ fs.writeFileSync(tmp, kept.join('\n') + '\n', 'utf-8');
212
+ const corrupt = path.join(dir, `${path.basename(jsonlPath)}.corrupt-${Date.now()}`);
213
+ fs.renameSync(jsonlPath, corrupt);
214
+ fs.renameSync(tmp, jsonlPath);
215
+ const { orphans: orphansAfter } = countOrphansInJsonl(jsonlPath);
216
+ debugLog('SURGICAL_COMPLETE', 'Surgical repair finished', {
217
+ removed,
218
+ orphansAfter,
219
+ corruptBackup: corrupt,
220
+ });
221
+ return { removed, orphansAfter };
222
+ }
223
+ /**
224
+ * Main entry point: repair or rollback a transcript with orphan tool_results.
225
+ *
226
+ * Strategy:
227
+ * 1. Check if there are orphans (exit early if none)
228
+ * 2. Try rollback to newest valid backup
229
+ * 3. If no valid backup, surgically remove orphan lines
230
+ */
231
+ function repairOrRollbackTranscript(jsonlPath) {
232
+ debugLog('REPAIR_START', '═══════════════════════════════════════════════════════════', {
233
+ action: 'TRANSCRIPT_REPAIR_INITIATED',
234
+ jsonlPath,
235
+ timestamp: new Date().toISOString(),
236
+ });
237
+ if (!fs.existsSync(jsonlPath)) {
238
+ debugLog('REPAIR_ABORT', 'JSONL file not found', { jsonlPath });
239
+ return { action: 'failed', orphansFound: 0, reason: 'jsonl_not_found' };
240
+ }
241
+ const initial = countOrphansInJsonl(jsonlPath);
242
+ debugLog('REPAIR_SCAN', 'Initial orphan count', {
243
+ orphans: initial.orphans,
244
+ orphanIds: initial.orphanIds,
245
+ });
246
+ if (initial.orphans === 0) {
247
+ debugLog('REPAIR_CLEAN', 'No orphans found - transcript is healthy');
248
+ return { action: 'none', orphansFound: 0 };
249
+ }
250
+ // 1) Try rollback to newest valid backup
251
+ const backups = findBackups(jsonlPath);
252
+ debugLog('REPAIR_BACKUPS', `Found ${backups.length} backup candidates`, { backups });
253
+ for (const backup of backups) {
254
+ try {
255
+ const v = countOrphansInJsonl(backup);
256
+ debugLog('REPAIR_CHECK_BACKUP', 'Validating backup', {
257
+ backup,
258
+ orphans: v.orphans,
259
+ });
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
+ }
272
+ atomicReplace(jsonlPath, backup);
273
+ debugLog('REPAIR_ROLLBACK_SUCCESS', 'Rolled back to valid backup', { backup });
274
+ return { action: 'rollback', orphansFound: initial.orphans, backupUsed: backup };
275
+ }
276
+ }
277
+ catch (err) {
278
+ debugLog('REPAIR_BACKUP_ERROR', 'Failed to check backup', {
279
+ backup,
280
+ error: err.message,
281
+ });
282
+ }
283
+ }
284
+ // 2) Surgical repair fallback
285
+ debugLog('REPAIR_SURGICAL', 'No valid backup found - attempting surgical repair');
286
+ const { removed, orphansAfter } = surgicalRepair(jsonlPath);
287
+ if (orphansAfter === 0) {
288
+ return { action: 'surgical_repair', orphansFound: initial.orphans, removedLines: removed };
289
+ }
290
+ debugLog('REPAIR_FAILED', 'Surgical repair incomplete - orphans remain', {
291
+ orphansAfter,
292
+ removed,
293
+ });
294
+ return {
295
+ action: 'failed',
296
+ orphansFound: initial.orphans,
297
+ removedLines: removed,
298
+ reason: 'orphans_remain_after_surgical',
299
+ };
300
+ }
301
+ /**
302
+ * Quick validation check - can be called before operations to verify health.
303
+ */
304
+ function isTranscriptHealthy(jsonlPath) {
305
+ if (!fs.existsSync(jsonlPath))
306
+ return true; // Non-existent is not unhealthy
307
+ const { orphans } = countOrphansInJsonl(jsonlPath);
308
+ return orphans === 0;
309
+ }
310
+ /**
311
+ * Get detailed transcript health info.
312
+ */
313
+ function getTranscriptHealth(jsonlPath) {
314
+ if (!fs.existsSync(jsonlPath)) {
315
+ return { exists: false, healthy: true, orphans: 0, orphanIds: [] };
316
+ }
317
+ const { orphans, orphanIds } = countOrphansInJsonl(jsonlPath);
318
+ return { exists: true, healthy: orphans === 0, orphans, orphanIds };
319
+ }
@@ -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;