@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
package/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # @ekkos/cli
2
+
3
+ Persistent cognitive memory for AI coding assistants. ekkOS gives Claude Code, Cursor, and Windsurf long-term memory — patterns, decisions, and operational intelligence that compound across every interaction.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ # Install globally
9
+ npm install -g @ekkos/cli
10
+
11
+ # Authenticate and configure your IDE
12
+ ekkos init
13
+
14
+ # Launch Claude Code with memory
15
+ ekkos run
16
+ ```
17
+
18
+ ## Commands
19
+
20
+ | Command | Description |
21
+ |---------|-------------|
22
+ | `ekkos init` | Authenticate and configure your IDE (Claude Code, Cursor, Windsurf) |
23
+ | `ekkos run` | Launch Claude Code with ekkOS memory connected |
24
+ | `ekkos run --dashboard` | Launch with live metrics dashboard (tmux split) |
25
+ | `ekkos status` | Check connection and memory health |
26
+ | `ekkos dashboard` | Open the live session dashboard |
27
+ | `ekkos export` | Export your patterns and memory as JSON |
28
+ | `ekkos --help` | Show all commands |
29
+
30
+ ## What It Does
31
+
32
+ - **Remembers what works** — Patterns from successful solutions are recalled automatically
33
+ - **Learns from mistakes** — Anti-patterns prevent repeating the same errors
34
+ - **Crosses sessions** — Knowledge persists across conversations, projects, and tools
35
+ - **Gets smarter over time** — Success rates compound as patterns are verified
36
+
37
+ ## How It Works
38
+
39
+ ekkOS installs hooks into your AI coding assistant that:
40
+
41
+ 1. **Before each prompt** — Searches 11 memory layers for relevant patterns
42
+ 2. **After each response** — Captures new solutions as reusable patterns
43
+ 3. **Between sessions** — Preserves context through eviction and rehydration
44
+
45
+ ## Requirements
46
+
47
+ - Node.js >= 18
48
+ - An ekkOS account ([platform.ekkos.dev](https://platform.ekkos.dev/signup))
49
+ - One of: Claude Code, Cursor, Windsurf, or VS Code
50
+
51
+ ## Documentation
52
+
53
+ [docs.ekkos.dev](https://docs.ekkos.dev)
54
+
55
+ ## License
56
+
57
+ Proprietary — see [ekkos.dev/terms](https://ekkos.dev/terms)
@@ -18,9 +18,16 @@ export declare class AgentDaemon {
18
18
  private ws;
19
19
  private reconnectAttempt;
20
20
  private heartbeatTimer;
21
+ private pongTimer;
22
+ private pongReceived;
21
23
  private ptyRunner;
22
24
  private currentSessionId;
25
+ private currentSessionCwd;
26
+ private sessionStartedAt;
27
+ private ptyRestartAttempts;
28
+ private readonly QUICK_EXIT_MS;
23
29
  private running;
30
+ private logPath;
24
31
  private outputBuffer;
25
32
  private currentSessionName;
26
33
  private isAutoClearInProgress;
@@ -63,6 +70,14 @@ export declare class AgentDaemon {
63
70
  * Handle resize from browser
64
71
  */
65
72
  private handleResize;
73
+ /**
74
+ * Handle directory listing request from relay for project browsing UI.
75
+ */
76
+ private handleListDirectories;
77
+ private resolveBrowsePath;
78
+ private getParentPath;
79
+ private getWindowsRootEntries;
80
+ private resolveSessionCwd;
66
81
  /**
67
82
  * Handle PTY exit
68
83
  */
@@ -83,10 +98,22 @@ export declare class AgentDaemon {
83
98
  * Sleep helper
84
99
  */
85
100
  private sleep;
101
+ /**
102
+ * Start or restart PTY for the current session.
103
+ */
104
+ private startSessionPty;
86
105
  /**
87
106
  * Handle WebSocket close
107
+ *
108
+ * CRITICAL: Do NOT kill the PTY here. The PTY must survive WebSocket
109
+ * disconnects so users can reconnect to their existing session.
110
+ * The PTY is only killed on explicit session_end or daemon stop.
88
111
  */
89
112
  private handleClose;
113
+ /**
114
+ * Schedule reconnection with exponential backoff
115
+ */
116
+ private scheduleReconnect;
90
117
  /**
91
118
  * Handle WebSocket error
92
119
  */
@@ -51,8 +51,10 @@ const os = __importStar(require("os"));
51
51
  const fs = __importStar(require("fs"));
52
52
  const path = __importStar(require("path"));
53
53
  const pty_runner_1 = require("./pty-runner");
54
+ const log_rotate_1 = require("../utils/log-rotate");
54
55
  const RELAY_URL = process.env.RELAY_WS_URL || 'wss://ekkos-relay-production.up.railway.app';
55
- const HEARTBEAT_INTERVAL = 30000; // 30 seconds
56
+ const HEARTBEAT_INTERVAL = 10000; // 10 seconds - must be well under Railway's 20-30s idle timeout
57
+ const PONG_TIMEOUT = 15000; // If no pong received in 15s, consider connection dead
56
58
  const RECONNECT_DELAYS = [1000, 2000, 4000, 8000, 16000, 32000, 60000]; // Exponential backoff
57
59
  // Auto-continue: Context wall detection pattern
58
60
  const CONTEXT_WALL_REGEX = /context limit reached.*\/(compact|clear)\b.*to continue/i;
@@ -65,8 +67,13 @@ class AgentDaemon {
65
67
  this.ws = null;
66
68
  this.reconnectAttempt = 0;
67
69
  this.heartbeatTimer = null;
70
+ this.pongTimer = null;
71
+ this.pongReceived = true;
68
72
  this.ptyRunner = null;
69
73
  this.currentSessionId = null;
74
+ this.sessionStartedAt = 0;
75
+ this.ptyRestartAttempts = 0;
76
+ this.QUICK_EXIT_MS = 15000;
70
77
  this.running = false;
71
78
  // Auto-continue state
72
79
  this.outputBuffer = '';
@@ -75,6 +82,7 @@ class AgentDaemon {
75
82
  this.lastContextWallTime = 0;
76
83
  this.CONTEXT_WALL_COOLDOWN = 30000; // 30 seconds between auto-clears
77
84
  this.config = config;
85
+ this.logPath = path.join(os.homedir(), '.ekkos', 'agent.log');
78
86
  }
79
87
  /**
80
88
  * Start the daemon
@@ -94,6 +102,11 @@ class AgentDaemon {
94
102
  clearInterval(this.heartbeatTimer);
95
103
  this.heartbeatTimer = null;
96
104
  }
105
+ // Stop pong timer
106
+ if (this.pongTimer) {
107
+ clearTimeout(this.pongTimer);
108
+ this.pongTimer = null;
109
+ }
97
110
  // Close PTY
98
111
  if (this.ptyRunner) {
99
112
  this.ptyRunner.kill();
@@ -121,6 +134,7 @@ class AgentDaemon {
121
134
  });
122
135
  this.ws.on('open', () => this.handleOpen());
123
136
  this.ws.on('message', (data) => this.handleMessage(data));
137
+ this.ws.on('pong', () => { this.pongReceived = true; });
124
138
  this.ws.on('close', (code, reason) => this.handleClose(code, reason.toString()));
125
139
  this.ws.on('error', (err) => this.handleError(err));
126
140
  }
@@ -130,8 +144,29 @@ class AgentDaemon {
130
144
  handleOpen() {
131
145
  this.log('Connected to relay server');
132
146
  this.reconnectAttempt = 0;
133
- // Start heartbeat
147
+ this.pongReceived = true;
148
+ // If we have a surviving PTY session, tell relay we're ready to reattach
149
+ if (this.ptyRunner && this.currentSessionId) {
150
+ this.log(`Re-advertising surviving session ${this.currentSessionId}`);
151
+ this.sendMessage({
152
+ type: 'session_alive',
153
+ sessionId: this.currentSessionId,
154
+ });
155
+ }
156
+ // Start heartbeat with dual ping: protocol-level + app-level
134
157
  this.heartbeatTimer = setInterval(() => {
158
+ if (!this.pongReceived) {
159
+ // Previous ping never got a pong - connection is dead
160
+ this.log('No pong received, connection dead - forcing reconnect');
161
+ this.ws?.terminate(); // terminate, not close - skip graceful shutdown
162
+ return;
163
+ }
164
+ this.pongReceived = false;
165
+ // Protocol-level ping (handled by ws library)
166
+ if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
167
+ this.ws.ping();
168
+ }
169
+ // App-level heartbeat (for relay server to track device presence)
135
170
  this.sendMessage({ type: 'heartbeat' });
136
171
  }, HEARTBEAT_INTERVAL);
137
172
  }
@@ -166,6 +201,9 @@ class AgentDaemon {
166
201
  case 'error':
167
202
  this.log('Server error:', message.error);
168
203
  break;
204
+ case 'list_dirs':
205
+ void this.handleListDirectories(message.requestId, message.path);
206
+ break;
169
207
  }
170
208
  }
171
209
  /**
@@ -173,27 +211,31 @@ class AgentDaemon {
173
211
  */
174
212
  handleSessionStart(sessionId, cwd) {
175
213
  this.log(`Session start request: ${sessionId}${cwd ? ` (cwd: ${cwd})` : ''}`);
176
- // Kill existing session if any
214
+ // If reconnecting to the SAME session, reuse existing PTY
215
+ if (this.ptyRunner && this.currentSessionId === sessionId) {
216
+ this.log(`Reattaching to existing PTY for session ${sessionId}`);
217
+ this.sendMessage({ type: 'ready', sessionId });
218
+ return;
219
+ }
220
+ // Different session - kill old PTY if any
177
221
  if (this.ptyRunner) {
222
+ this.log(`Killing old PTY for session ${this.currentSessionId}`);
178
223
  this.ptyRunner.kill();
179
224
  this.ptyRunner = null;
180
225
  }
181
226
  this.currentSessionId = sessionId;
227
+ const resolved = this.resolveSessionCwd(cwd);
228
+ this.currentSessionCwd = resolved.cwd;
229
+ this.ptyRestartAttempts = 0;
182
230
  // Reset auto-continue state
183
231
  this.outputBuffer = '';
184
232
  this.currentSessionName = null;
185
233
  this.isAutoClearInProgress = false;
186
234
  this.lastContextWallTime = 0;
187
- // Start PTY with ekkos run (skip -d flag to avoid double init)
188
- this.ptyRunner = new pty_runner_1.PTYRunner({
189
- command: 'ekkos',
190
- args: ['run', '-b'], // -b for bypass permissions (skip -d to avoid double spawn)
191
- onData: (data) => this.sendOutput(data),
192
- onExit: (code) => this.handlePTYExit(code),
193
- cwd: cwd || process.env.HOME, // Use specified cwd or fall back to home
194
- verbose: this.config.verbose,
195
- });
196
- this.ptyRunner.start();
235
+ this.startSessionPty(resolved.cwd);
236
+ if (resolved.warning) {
237
+ this.sendOutput(`\r\n[ekkOS] ${resolved.warning}\r\n`);
238
+ }
197
239
  // Notify server that PTY is ready
198
240
  this.sendMessage({
199
241
  type: 'ready',
@@ -208,11 +250,13 @@ class AgentDaemon {
208
250
  return; // Not our session
209
251
  }
210
252
  this.log('Session ended');
253
+ this.currentSessionId = null;
254
+ this.currentSessionCwd = undefined;
255
+ this.ptyRestartAttempts = 0;
211
256
  if (this.ptyRunner) {
212
257
  this.ptyRunner.kill();
213
258
  this.ptyRunner = null;
214
259
  }
215
- this.currentSessionId = null;
216
260
  }
217
261
  /**
218
262
  * Handle input from browser
@@ -230,17 +274,162 @@ class AgentDaemon {
230
274
  this.ptyRunner.resize(cols, rows);
231
275
  }
232
276
  }
277
+ /**
278
+ * Handle directory listing request from relay for project browsing UI.
279
+ */
280
+ async handleListDirectories(requestId, requestedPath) {
281
+ if (!requestId)
282
+ return;
283
+ try {
284
+ const resolvedPath = this.resolveBrowsePath(requestedPath);
285
+ if (resolvedPath === '__WINDOWS_ROOT__') {
286
+ this.sendMessage({
287
+ type: 'list_dirs_result',
288
+ requestId,
289
+ path: '/',
290
+ parentPath: null,
291
+ entries: this.getWindowsRootEntries(),
292
+ });
293
+ return;
294
+ }
295
+ const dirents = await fs.promises.readdir(resolvedPath, { withFileTypes: true });
296
+ const entries = dirents
297
+ .filter((entry) => entry.isDirectory())
298
+ .map((entry) => ({
299
+ name: entry.name,
300
+ path: path.join(resolvedPath, entry.name),
301
+ }))
302
+ .sort((a, b) => a.name.localeCompare(b.name))
303
+ .slice(0, 300);
304
+ this.sendMessage({
305
+ type: 'list_dirs_result',
306
+ requestId,
307
+ path: resolvedPath,
308
+ parentPath: this.getParentPath(resolvedPath),
309
+ entries,
310
+ });
311
+ }
312
+ catch (error) {
313
+ const message = error instanceof Error ? error.message : 'Failed to read directory';
314
+ this.sendMessage({
315
+ type: 'list_dirs_result',
316
+ requestId,
317
+ error: message,
318
+ });
319
+ }
320
+ }
321
+ resolveBrowsePath(requestedPath) {
322
+ const homePath = os.homedir();
323
+ const raw = requestedPath?.trim() ?? '';
324
+ if (process.platform === 'win32' && (raw === '' || raw === '/' || raw === '\\')) {
325
+ return '__WINDOWS_ROOT__';
326
+ }
327
+ if (!raw) {
328
+ return homePath;
329
+ }
330
+ if (raw === '~') {
331
+ return homePath;
332
+ }
333
+ if (raw.startsWith('~/') || raw.startsWith('~\\')) {
334
+ return path.resolve(path.join(homePath, raw.slice(2)));
335
+ }
336
+ return path.resolve(raw);
337
+ }
338
+ getParentPath(currentPath) {
339
+ if (process.platform === 'win32' && /^[A-Za-z]:\\?$/.test(currentPath)) {
340
+ return null;
341
+ }
342
+ const parsed = path.parse(currentPath);
343
+ if (currentPath === parsed.root) {
344
+ return null;
345
+ }
346
+ const parent = path.dirname(currentPath);
347
+ return parent === currentPath ? null : parent;
348
+ }
349
+ getWindowsRootEntries() {
350
+ const entries = [];
351
+ for (let code = 65; code <= 90; code++) {
352
+ const drive = String.fromCharCode(code);
353
+ const drivePath = `${drive}:\\`;
354
+ if (fs.existsSync(drivePath)) {
355
+ entries.push({
356
+ name: `${drive}:`,
357
+ path: drivePath,
358
+ });
359
+ }
360
+ }
361
+ return entries;
362
+ }
363
+ resolveSessionCwd(cwd) {
364
+ const homePath = os.homedir();
365
+ const raw = cwd?.trim();
366
+ if (!raw) {
367
+ return { cwd: homePath };
368
+ }
369
+ let expanded = raw;
370
+ if (raw === '~') {
371
+ expanded = homePath;
372
+ }
373
+ else if (raw.startsWith('~/') || raw.startsWith('~\\')) {
374
+ expanded = path.join(homePath, raw.slice(2));
375
+ }
376
+ const resolvedPath = path.resolve(expanded);
377
+ try {
378
+ const stat = fs.statSync(resolvedPath);
379
+ if (stat.isDirectory()) {
380
+ return { cwd: resolvedPath };
381
+ }
382
+ return {
383
+ cwd: homePath,
384
+ warning: `Requested path is not a directory: ${raw}. Falling back to ${homePath}.`,
385
+ };
386
+ }
387
+ catch {
388
+ return {
389
+ cwd: homePath,
390
+ warning: `Requested path not found: ${raw}. Falling back to ${homePath}.`,
391
+ };
392
+ }
393
+ }
233
394
  /**
234
395
  * Handle PTY exit
235
396
  */
236
397
  handlePTYExit(code) {
237
398
  this.log(`PTY exited with code ${code}`);
399
+ const elapsed = Date.now() - this.sessionStartedAt;
400
+ const hasActiveSession = this.running && this.currentSessionId !== null;
401
+ const quickExit = elapsed < this.QUICK_EXIT_MS;
402
+ if (hasActiveSession && code !== 0) {
403
+ this.ptyRestartAttempts = quickExit ? this.ptyRestartAttempts + 1 : 1;
404
+ const attempt = this.ptyRestartAttempts;
405
+ const sessionId = this.currentSessionId;
406
+ const restartDelay = quickExit
407
+ ? Math.min(600 * attempt, 5000)
408
+ : 1000;
409
+ const reason = quickExit ? 'crashed' : 'exited unexpectedly';
410
+ this.log(`PTY ${reason} (${elapsed}ms, code ${code}). Restarting in ${restartDelay}ms (attempt ${attempt})...`);
411
+ this.ptyRunner = null;
412
+ this.sendOutput(`\r\n[ekkOS] Terminal ${reason} (code ${code}). Restarting in ${Math.max(1, Math.ceil(restartDelay / 1000))}s...\r\n`);
413
+ setTimeout(() => {
414
+ if (!this.running || !sessionId || this.currentSessionId !== sessionId || this.ptyRunner) {
415
+ return;
416
+ }
417
+ this.startSessionPty(this.currentSessionCwd);
418
+ this.sendMessage({
419
+ type: 'ready',
420
+ sessionId,
421
+ });
422
+ }, restartDelay);
423
+ return;
424
+ }
238
425
  this.sendMessage({
239
426
  type: 'session_end',
240
427
  sessionId: this.currentSessionId || undefined,
241
428
  });
242
429
  this.ptyRunner = null;
243
430
  this.currentSessionId = null;
431
+ this.currentSessionCwd = undefined;
432
+ this.ptyRestartAttempts = 0;
244
433
  }
245
434
  /**
246
435
  * Send PTY output to server (with auto-continue detection)
@@ -318,8 +507,39 @@ class AgentDaemon {
318
507
  sleep(ms) {
319
508
  return new Promise(resolve => setTimeout(resolve, ms));
320
509
  }
510
+ /**
511
+ * Start or restart PTY for the current session.
512
+ */
513
+ startSessionPty(cwd) {
514
+ this.sessionStartedAt = Date.now();
515
+ // Run through the same Node runtime as the daemon to avoid PATH/shebang drift
516
+ // (e.g. Homebrew node upgrades breaking dylib links).
517
+ const fallbackEntrypoint = path.resolve(__dirname, '..', 'index.js');
518
+ const argvEntrypoint = process.argv[1];
519
+ const cliEntrypoint = argvEntrypoint && !argvEntrypoint.includes('/node_modules/.bin/')
520
+ ? argvEntrypoint
521
+ : fallbackEntrypoint;
522
+ // Start PTY with `node <cli> run -b` (skip -d to avoid double spawn).
523
+ this.ptyRunner = new pty_runner_1.PTYRunner({
524
+ command: process.execPath,
525
+ args: [cliEntrypoint, 'run', '-b'],
526
+ onData: (data) => this.sendOutput(data),
527
+ onExit: (code) => this.handlePTYExit(code),
528
+ cwd: cwd || process.env.HOME, // Use specified cwd or fall back to home
529
+ env: {
530
+ EKKOS_REMOTE_SESSION: '1',
531
+ EKKOS_NO_SPLASH: '1',
532
+ },
533
+ verbose: this.config.verbose,
534
+ });
535
+ this.ptyRunner.start();
536
+ }
321
537
  /**
322
538
  * Handle WebSocket close
539
+ *
540
+ * CRITICAL: Do NOT kill the PTY here. The PTY must survive WebSocket
541
+ * disconnects so users can reconnect to their existing session.
542
+ * The PTY is only killed on explicit session_end or daemon stop.
323
543
  */
324
544
  handleClose(code, reason) {
325
545
  this.log(`Disconnected: ${code} ${reason}`);
@@ -328,20 +548,29 @@ class AgentDaemon {
328
548
  clearInterval(this.heartbeatTimer);
329
549
  this.heartbeatTimer = null;
330
550
  }
331
- // Kill PTY if running
332
- if (this.ptyRunner) {
333
- this.ptyRunner.kill();
334
- this.ptyRunner = null;
551
+ if (this.pongTimer) {
552
+ clearTimeout(this.pongTimer);
553
+ this.pongTimer = null;
554
+ }
555
+ // PTY stays alive - user can reconnect to existing session
556
+ // Only log if there's an active session being preserved
557
+ if (this.ptyRunner && this.currentSessionId) {
558
+ this.log(`Preserving PTY session ${this.currentSessionId} across disconnect`);
335
559
  }
336
- this.currentSessionId = null;
337
560
  // Reconnect if still running
338
561
  if (this.running) {
339
- const delay = RECONNECT_DELAYS[Math.min(this.reconnectAttempt, RECONNECT_DELAYS.length - 1)];
340
- this.reconnectAttempt++;
341
- this.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempt})...`);
342
- setTimeout(() => this.connect(), delay);
562
+ this.scheduleReconnect();
343
563
  }
344
564
  }
565
+ /**
566
+ * Schedule reconnection with exponential backoff
567
+ */
568
+ scheduleReconnect() {
569
+ const delay = RECONNECT_DELAYS[Math.min(this.reconnectAttempt, RECONNECT_DELAYS.length - 1)];
570
+ this.reconnectAttempt++;
571
+ this.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempt})...`);
572
+ setTimeout(() => this.connect(), delay);
573
+ }
345
574
  /**
346
575
  * Handle WebSocket error
347
576
  */
@@ -365,13 +594,9 @@ class AgentDaemon {
365
594
  if (this.config.verbose) {
366
595
  console.log(message);
367
596
  }
368
- // Also log to file
597
+ // Log to file with rotation
369
598
  try {
370
- const logDir = path.join(os.homedir(), '.ekkos');
371
- if (!fs.existsSync(logDir)) {
372
- fs.mkdirSync(logDir, { recursive: true });
373
- }
374
- fs.appendFileSync(path.join(logDir, 'agent.log'), message + '\n');
599
+ (0, log_rotate_1.appendLog)(this.logPath, message);
375
600
  }
376
601
  catch {
377
602
  // Ignore log errors
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Health check for ekkOS agent daemon
3
+ *
4
+ * Verifies:
5
+ * - Service is installed and loaded
6
+ * - Process is running
7
+ * - Recent activity in logs
8
+ * - Network connectivity to relay server
9
+ */
10
+ interface HealthStatus {
11
+ ok: boolean;
12
+ service: {
13
+ installed: boolean;
14
+ loaded: boolean;
15
+ running: boolean;
16
+ pid?: number;
17
+ };
18
+ logs: {
19
+ lastActivity?: Date;
20
+ recentErrors: string[];
21
+ };
22
+ relay: {
23
+ reachable: boolean;
24
+ lastError?: string;
25
+ };
26
+ }
27
+ /**
28
+ * Check agent daemon health
29
+ */
30
+ export declare function checkDaemonHealth(): Promise<HealthStatus>;
31
+ /**
32
+ * Format health status for console output
33
+ */
34
+ export declare function formatHealthStatus(status: HealthStatus): string;
35
+ export {};