@ekkos/cli 1.3.1 → 1.3.5

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 (131) hide show
  1. package/dist/capture/jsonl-rewriter.d.ts +1 -1
  2. package/dist/capture/jsonl-rewriter.js +3 -3
  3. package/dist/capture/transcript-repair.d.ts +2 -2
  4. package/dist/capture/transcript-repair.js +2 -2
  5. package/dist/commands/claw.d.ts +13 -0
  6. package/dist/commands/claw.js +253 -0
  7. package/dist/commands/dashboard.js +742 -118
  8. package/dist/commands/doctor.d.ts +3 -3
  9. package/dist/commands/doctor.js +6 -79
  10. package/dist/commands/gemini.d.ts +19 -0
  11. package/dist/commands/gemini.js +193 -0
  12. package/dist/commands/init.d.ts +1 -0
  13. package/dist/commands/init.js +56 -41
  14. package/dist/commands/run.d.ts +0 -1
  15. package/dist/commands/run.js +288 -263
  16. package/dist/commands/scan.d.ts +21 -0
  17. package/dist/commands/scan.js +386 -0
  18. package/dist/commands/status.d.ts +4 -1
  19. package/dist/commands/status.js +165 -27
  20. package/dist/commands/swarm-dashboard.js +156 -28
  21. package/dist/commands/swarm.d.ts +1 -1
  22. package/dist/commands/swarm.js +1 -1
  23. package/dist/commands/test-claude.d.ts +2 -2
  24. package/dist/commands/test-claude.js +3 -3
  25. package/dist/deploy/index.d.ts +0 -2
  26. package/dist/deploy/index.js +0 -2
  27. package/dist/deploy/settings.d.ts +6 -5
  28. package/dist/deploy/settings.js +64 -16
  29. package/dist/deploy/skills.js +1 -2
  30. package/dist/index.js +86 -96
  31. package/dist/lib/usage-parser.d.ts +1 -1
  32. package/dist/lib/usage-parser.js +9 -6
  33. package/dist/local/index.d.ts +14 -0
  34. package/dist/local/index.js +28 -0
  35. package/dist/local/local-embeddings.d.ts +49 -0
  36. package/dist/local/local-embeddings.js +232 -0
  37. package/dist/local/offline-fallback.d.ts +44 -0
  38. package/dist/local/offline-fallback.js +159 -0
  39. package/dist/local/sqlite-store.d.ts +126 -0
  40. package/dist/local/sqlite-store.js +393 -0
  41. package/dist/local/sync-engine.d.ts +42 -0
  42. package/dist/local/sync-engine.js +223 -0
  43. package/dist/utils/platform.d.ts +5 -1
  44. package/dist/utils/platform.js +24 -4
  45. package/dist/utils/proxy-url.d.ts +21 -0
  46. package/dist/utils/proxy-url.js +34 -0
  47. package/dist/utils/state.d.ts +1 -1
  48. package/dist/utils/state.js +11 -3
  49. package/dist/utils/templates.js +1 -1
  50. package/package.json +11 -4
  51. package/templates/CLAUDE.md +49 -107
  52. package/dist/agent/daemon.d.ts +0 -130
  53. package/dist/agent/daemon.js +0 -606
  54. package/dist/agent/health-check.d.ts +0 -35
  55. package/dist/agent/health-check.js +0 -243
  56. package/dist/agent/pty-runner.d.ts +0 -53
  57. package/dist/agent/pty-runner.js +0 -190
  58. package/dist/commands/agent.d.ts +0 -50
  59. package/dist/commands/agent.js +0 -544
  60. package/dist/commands/setup-remote.d.ts +0 -20
  61. package/dist/commands/setup-remote.js +0 -582
  62. package/dist/utils/verify-remote-terminal.d.ts +0 -10
  63. package/dist/utils/verify-remote-terminal.js +0 -415
  64. package/templates/README.md +0 -378
  65. package/templates/claude-plugins/PHASE2_COMPLETION.md +0 -346
  66. package/templates/claude-plugins/PLUGIN_PROPOSALS.md +0 -1776
  67. package/templates/claude-plugins/README.md +0 -587
  68. package/templates/claude-plugins/agents/code-reviewer.json +0 -14
  69. package/templates/claude-plugins/agents/debug-detective.json +0 -15
  70. package/templates/claude-plugins/agents/git-companion.json +0 -14
  71. package/templates/claude-plugins/blog-manager/.claude-plugin/plugin.json +0 -8
  72. package/templates/claude-plugins/blog-manager/commands/blog.md +0 -691
  73. package/templates/claude-plugins/golden-loop-monitor/.claude-plugin/plugin.json +0 -8
  74. package/templates/claude-plugins/golden-loop-monitor/commands/loop-status.md +0 -434
  75. package/templates/claude-plugins/learning-tracker/.claude-plugin/plugin.json +0 -8
  76. package/templates/claude-plugins/learning-tracker/commands/my-patterns.md +0 -282
  77. package/templates/claude-plugins/memory-lens/.claude-plugin/plugin.json +0 -8
  78. package/templates/claude-plugins/memory-lens/commands/memory-search.md +0 -181
  79. package/templates/claude-plugins/pattern-coach/.claude-plugin/plugin.json +0 -8
  80. package/templates/claude-plugins/pattern-coach/commands/forge.md +0 -365
  81. package/templates/claude-plugins/project-schema-validator/.claude-plugin/plugin.json +0 -8
  82. package/templates/claude-plugins/project-schema-validator/commands/validate-schema.md +0 -582
  83. package/templates/claude-plugins-admin/AGENT_TEAM_PROPOSALS.md +0 -819
  84. package/templates/claude-plugins-admin/README.md +0 -446
  85. package/templates/claude-plugins-admin/autonomous-admin-agent/.claude-plugin/plugin.json +0 -8
  86. package/templates/claude-plugins-admin/autonomous-admin-agent/commands/agent.md +0 -595
  87. package/templates/claude-plugins-admin/backend-agent/.claude-plugin/plugin.json +0 -8
  88. package/templates/claude-plugins-admin/backend-agent/commands/backend.md +0 -798
  89. package/templates/claude-plugins-admin/deploy-guardian/.claude-plugin/plugin.json +0 -8
  90. package/templates/claude-plugins-admin/deploy-guardian/commands/deploy.md +0 -554
  91. package/templates/claude-plugins-admin/frontend-agent/.claude-plugin/plugin.json +0 -8
  92. package/templates/claude-plugins-admin/frontend-agent/commands/frontend.md +0 -881
  93. package/templates/claude-plugins-admin/mcp-server-manager/.claude-plugin/plugin.json +0 -8
  94. package/templates/claude-plugins-admin/mcp-server-manager/commands/mcp.md +0 -85
  95. package/templates/claude-plugins-admin/memory-system-monitor/.claude-plugin/plugin.json +0 -8
  96. package/templates/claude-plugins-admin/memory-system-monitor/commands/memory-health.md +0 -569
  97. package/templates/claude-plugins-admin/qa-agent/.claude-plugin/plugin.json +0 -8
  98. package/templates/claude-plugins-admin/qa-agent/commands/qa.md +0 -863
  99. package/templates/claude-plugins-admin/tech-lead-agent/.claude-plugin/plugin.json +0 -8
  100. package/templates/claude-plugins-admin/tech-lead-agent/commands/lead.md +0 -732
  101. package/templates/commands/continue.md +0 -47
  102. package/templates/cursor-rules/ekkos-memory.md +0 -127
  103. package/templates/ekkos-manifest.json +0 -223
  104. package/templates/helpers/json-parse.cjs +0 -101
  105. package/templates/hooks-node/lib/state.js +0 -187
  106. package/templates/hooks-node/stop.js +0 -416
  107. package/templates/hooks-node/user-prompt-submit.js +0 -337
  108. package/templates/plan-template.md +0 -306
  109. package/templates/rules/00-hooks-contract.mdc +0 -89
  110. package/templates/rules/30-ekkos-core.mdc +0 -188
  111. package/templates/rules/31-ekkos-messages.mdc +0 -78
  112. package/templates/shared/hooks-enabled.json +0 -22
  113. package/templates/shared/session-words.json +0 -45
  114. package/templates/skills/ekkOS_Deep_Recall/Skill.md +0 -282
  115. package/templates/skills/ekkOS_Learn/Skill.md +0 -265
  116. package/templates/skills/ekkOS_Memory_First/Skill.md +0 -206
  117. package/templates/skills/ekkOS_Plan_Assist/Skill.md +0 -302
  118. package/templates/skills/ekkOS_Preferences/Skill.md +0 -247
  119. package/templates/skills/ekkOS_Reflect/Skill.md +0 -257
  120. package/templates/skills/ekkOS_Safety/Skill.md +0 -265
  121. package/templates/skills/ekkOS_Schema/Skill.md +0 -251
  122. package/templates/skills/ekkOS_Summary/Skill.md +0 -257
  123. package/templates/spec-template.md +0 -159
  124. package/templates/windsurf-rules/ekkos-memory.md +0 -127
  125. package/templates/windsurf-skills/README.md +0 -58
  126. package/templates/windsurf-skills/ekkos-continue/SKILL.md +0 -81
  127. package/templates/windsurf-skills/ekkos-golden-loop/SKILL.md +0 -225
  128. package/templates/windsurf-skills/ekkos-insights/SKILL.md +0 -138
  129. package/templates/windsurf-skills/ekkos-recall/SKILL.md +0 -96
  130. package/templates/windsurf-skills/ekkos-safety/SKILL.md +0 -89
  131. package/templates/windsurf-skills/ekkos-vault/SKILL.md +0 -86
@@ -1,606 +0,0 @@
1
- "use strict";
2
- /**
3
- * ekkOS Agent Daemon
4
- *
5
- * Background daemon that:
6
- * 1. Maintains WebSocket connection to cloud relay
7
- * 2. Receives session start requests
8
- * 3. Spawns `ekkos run -d` in PTY
9
- * 4. Relays PTY I/O to cloud
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.AgentDaemon = void 0;
49
- const ws_1 = __importDefault(require("ws"));
50
- const os = __importStar(require("os"));
51
- const fs = __importStar(require("fs"));
52
- const path = __importStar(require("path"));
53
- const pty_runner_1 = require("./pty-runner");
54
- const log_rotate_1 = require("../utils/log-rotate");
55
- const RELAY_URL = process.env.RELAY_WS_URL || 'wss://ekkos-relay-production.up.railway.app';
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
58
- const RECONNECT_DELAYS = [1000, 2000, 4000, 8000, 16000, 32000, 60000]; // Exponential backoff
59
- // Auto-continue: Context wall detection pattern
60
- const CONTEXT_WALL_REGEX = /context limit reached.*\/(compact|clear)\b.*to continue/i;
61
- // Session name detection pattern (3-word slug: word-word-word)
62
- const SESSION_NAME_REGEX = /\b([a-z]+-[a-z]+-[a-z]+)\b/i;
63
- // Idle prompt detection - Claude shows "> " when ready for input
64
- const IDLE_PROMPT_REGEX = />\s*$/;
65
- class AgentDaemon {
66
- constructor(config) {
67
- this.ws = null;
68
- this.reconnectAttempt = 0;
69
- this.heartbeatTimer = null;
70
- this.pongTimer = null;
71
- this.pongReceived = true;
72
- this.ptyRunner = null;
73
- this.currentSessionId = null;
74
- this.sessionStartedAt = 0;
75
- this.ptyRestartAttempts = 0;
76
- this.QUICK_EXIT_MS = 15000;
77
- this.running = false;
78
- // Auto-continue state
79
- this.outputBuffer = '';
80
- this.currentSessionName = null;
81
- this.isAutoClearInProgress = false;
82
- this.lastContextWallTime = 0;
83
- this.CONTEXT_WALL_COOLDOWN = 30000; // 30 seconds between auto-clears
84
- this.config = config;
85
- this.logPath = path.join(os.homedir(), '.ekkos', 'agent.log');
86
- }
87
- /**
88
- * Start the daemon
89
- */
90
- async start() {
91
- this.running = true;
92
- this.log('Starting ekkOS agent daemon...');
93
- this.connect();
94
- }
95
- /**
96
- * Stop the daemon
97
- */
98
- async stop() {
99
- this.running = false;
100
- // Stop heartbeat
101
- if (this.heartbeatTimer) {
102
- clearInterval(this.heartbeatTimer);
103
- this.heartbeatTimer = null;
104
- }
105
- // Stop pong timer
106
- if (this.pongTimer) {
107
- clearTimeout(this.pongTimer);
108
- this.pongTimer = null;
109
- }
110
- // Close PTY
111
- if (this.ptyRunner) {
112
- this.ptyRunner.kill();
113
- this.ptyRunner = null;
114
- }
115
- // Close WebSocket
116
- if (this.ws) {
117
- this.ws.close();
118
- this.ws = null;
119
- }
120
- this.log('Daemon stopped');
121
- }
122
- /**
123
- * Connect to relay server
124
- */
125
- connect() {
126
- if (!this.running)
127
- return;
128
- const url = `${RELAY_URL}/api/v1/relay/device`;
129
- this.log(`Connecting to ${url}...`);
130
- this.ws = new ws_1.default(url, {
131
- headers: {
132
- 'X-Device-Token': this.config.deviceToken,
133
- },
134
- });
135
- this.ws.on('open', () => this.handleOpen());
136
- this.ws.on('message', (data) => this.handleMessage(data));
137
- this.ws.on('pong', () => { this.pongReceived = true; });
138
- this.ws.on('close', (code, reason) => this.handleClose(code, reason.toString()));
139
- this.ws.on('error', (err) => this.handleError(err));
140
- }
141
- /**
142
- * Handle WebSocket open
143
- */
144
- handleOpen() {
145
- this.log('Connected to relay server');
146
- this.reconnectAttempt = 0;
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
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)
170
- this.sendMessage({ type: 'heartbeat' });
171
- }, HEARTBEAT_INTERVAL);
172
- }
173
- /**
174
- * Handle incoming message
175
- */
176
- handleMessage(rawData) {
177
- let message;
178
- try {
179
- message = JSON.parse(rawData.toString());
180
- }
181
- catch (err) {
182
- this.log('Invalid message from server:', err);
183
- return;
184
- }
185
- switch (message.type) {
186
- case 'connected':
187
- this.log('Registered with relay');
188
- break;
189
- case 'session_start':
190
- this.handleSessionStart(message.sessionId, message.cwd);
191
- break;
192
- case 'session_end':
193
- this.handleSessionEnd(message.sessionId);
194
- break;
195
- case 'input':
196
- this.handleInput(message.data || '');
197
- break;
198
- case 'resize':
199
- this.handleResize(message.cols || 80, message.rows || 24);
200
- break;
201
- case 'error':
202
- this.log('Server error:', message.error);
203
- break;
204
- case 'list_dirs':
205
- void this.handleListDirectories(message.requestId, message.path);
206
- break;
207
- }
208
- }
209
- /**
210
- * Handle session start request
211
- */
212
- handleSessionStart(sessionId, cwd) {
213
- this.log(`Session start request: ${sessionId}${cwd ? ` (cwd: ${cwd})` : ''}`);
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
221
- if (this.ptyRunner) {
222
- this.log(`Killing old PTY for session ${this.currentSessionId}`);
223
- this.ptyRunner.kill();
224
- this.ptyRunner = null;
225
- }
226
- this.currentSessionId = sessionId;
227
- const resolved = this.resolveSessionCwd(cwd);
228
- this.currentSessionCwd = resolved.cwd;
229
- this.ptyRestartAttempts = 0;
230
- // Reset auto-continue state
231
- this.outputBuffer = '';
232
- this.currentSessionName = null;
233
- this.isAutoClearInProgress = false;
234
- this.lastContextWallTime = 0;
235
- this.startSessionPty(resolved.cwd);
236
- if (resolved.warning) {
237
- this.sendOutput(`\r\n[ekkOS] ${resolved.warning}\r\n`);
238
- }
239
- // Notify server that PTY is ready
240
- this.sendMessage({
241
- type: 'ready',
242
- sessionId,
243
- });
244
- }
245
- /**
246
- * Handle session end request
247
- */
248
- handleSessionEnd(sessionId) {
249
- if (sessionId && sessionId !== this.currentSessionId) {
250
- return; // Not our session
251
- }
252
- this.log('Session ended');
253
- this.currentSessionId = null;
254
- this.currentSessionCwd = undefined;
255
- this.ptyRestartAttempts = 0;
256
- if (this.ptyRunner) {
257
- this.ptyRunner.kill();
258
- this.ptyRunner = null;
259
- }
260
- }
261
- /**
262
- * Handle input from browser
263
- */
264
- handleInput(data) {
265
- if (this.ptyRunner) {
266
- this.ptyRunner.write(data);
267
- }
268
- }
269
- /**
270
- * Handle resize from browser
271
- */
272
- handleResize(cols, rows) {
273
- if (this.ptyRunner) {
274
- this.ptyRunner.resize(cols, rows);
275
- }
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
- }
394
- /**
395
- * Handle PTY exit
396
- */
397
- handlePTYExit(code) {
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
- }
425
- this.sendMessage({
426
- type: 'session_end',
427
- sessionId: this.currentSessionId || undefined,
428
- });
429
- this.ptyRunner = null;
430
- this.currentSessionId = null;
431
- this.currentSessionCwd = undefined;
432
- this.ptyRestartAttempts = 0;
433
- }
434
- /**
435
- * Send PTY output to server (with auto-continue detection)
436
- */
437
- sendOutput(data) {
438
- // Always forward output to server
439
- this.sendMessage({
440
- type: 'output',
441
- sessionId: this.currentSessionId || undefined,
442
- data,
443
- });
444
- // Auto-continue: Buffer output and detect context wall
445
- this.outputBuffer += data;
446
- // Keep buffer manageable (last 2KB)
447
- if (this.outputBuffer.length > 2048) {
448
- this.outputBuffer = this.outputBuffer.slice(-2048);
449
- }
450
- // Extract session name from output (e.g., "qix-fox-use" in hook output)
451
- const sessionMatch = this.outputBuffer.match(SESSION_NAME_REGEX);
452
- if (sessionMatch) {
453
- this.currentSessionName = sessionMatch[1];
454
- }
455
- // Detect context wall
456
- if (CONTEXT_WALL_REGEX.test(this.outputBuffer) && !this.isAutoClearInProgress) {
457
- const now = Date.now();
458
- if (now - this.lastContextWallTime > this.CONTEXT_WALL_COOLDOWN) {
459
- this.lastContextWallTime = now;
460
- this.triggerAutoContinue();
461
- }
462
- }
463
- }
464
- /**
465
- * Trigger auto /clear + /continue when context wall is hit
466
- */
467
- async triggerAutoContinue() {
468
- if (this.isAutoClearInProgress || !this.ptyRunner)
469
- return;
470
- this.isAutoClearInProgress = true;
471
- this.log('Auto-continue: Context wall detected, initiating /clear + /continue');
472
- // Wait for idle prompt
473
- await this.waitForIdlePrompt();
474
- // Type /clear
475
- this.log('Auto-continue: Sending /clear');
476
- this.ptyRunner.write('/clear\n');
477
- // Wait for clear to complete
478
- await this.sleep(2000);
479
- await this.waitForIdlePrompt();
480
- // Type /continue with session name
481
- const continueCmd = this.currentSessionName
482
- ? `/continue ${this.currentSessionName}\n`
483
- : '/continue\n';
484
- this.log(`Auto-continue: Sending ${continueCmd.trim()}`);
485
- this.ptyRunner.write(continueCmd);
486
- // Reset state
487
- this.outputBuffer = '';
488
- this.isAutoClearInProgress = false;
489
- this.log('Auto-continue: Complete');
490
- }
491
- /**
492
- * Wait for Claude's idle prompt ("> ")
493
- */
494
- async waitForIdlePrompt(timeout = 10000) {
495
- const startTime = Date.now();
496
- while (Date.now() - startTime < timeout) {
497
- if (IDLE_PROMPT_REGEX.test(this.outputBuffer)) {
498
- return;
499
- }
500
- await this.sleep(100);
501
- }
502
- this.log('Auto-continue: Timeout waiting for idle prompt');
503
- }
504
- /**
505
- * Sleep helper
506
- */
507
- sleep(ms) {
508
- return new Promise(resolve => setTimeout(resolve, ms));
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
- }
537
- /**
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.
543
- */
544
- handleClose(code, reason) {
545
- this.log(`Disconnected: ${code} ${reason}`);
546
- // Stop heartbeat
547
- if (this.heartbeatTimer) {
548
- clearInterval(this.heartbeatTimer);
549
- this.heartbeatTimer = null;
550
- }
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`);
559
- }
560
- // Reconnect if still running
561
- if (this.running) {
562
- this.scheduleReconnect();
563
- }
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
- }
574
- /**
575
- * Handle WebSocket error
576
- */
577
- handleError(err) {
578
- this.log('WebSocket error:', err.message);
579
- }
580
- /**
581
- * Send message to server
582
- */
583
- sendMessage(message) {
584
- if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
585
- this.ws.send(JSON.stringify(message));
586
- }
587
- }
588
- /**
589
- * Log message
590
- */
591
- log(...args) {
592
- const timestamp = new Date().toISOString();
593
- const message = `[${timestamp}] ${args.map(String).join(' ')}`;
594
- if (this.config.verbose) {
595
- console.log(message);
596
- }
597
- // Log to file with rotation
598
- try {
599
- (0, log_rotate_1.appendLog)(this.logPath, message);
600
- }
601
- catch {
602
- // Ignore log errors
603
- }
604
- }
605
- }
606
- exports.AgentDaemon = AgentDaemon;
@@ -1,35 +0,0 @@
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 {};