@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.
- package/dist/capture/jsonl-rewriter.d.ts +1 -1
- package/dist/capture/jsonl-rewriter.js +3 -3
- package/dist/capture/transcript-repair.d.ts +2 -2
- package/dist/capture/transcript-repair.js +2 -2
- package/dist/commands/claw.d.ts +13 -0
- package/dist/commands/claw.js +253 -0
- package/dist/commands/dashboard.js +742 -118
- package/dist/commands/doctor.d.ts +3 -3
- package/dist/commands/doctor.js +6 -79
- package/dist/commands/gemini.d.ts +19 -0
- package/dist/commands/gemini.js +193 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +56 -41
- package/dist/commands/run.d.ts +0 -1
- package/dist/commands/run.js +288 -263
- package/dist/commands/scan.d.ts +21 -0
- package/dist/commands/scan.js +386 -0
- package/dist/commands/status.d.ts +4 -1
- package/dist/commands/status.js +165 -27
- package/dist/commands/swarm-dashboard.js +156 -28
- package/dist/commands/swarm.d.ts +1 -1
- package/dist/commands/swarm.js +1 -1
- package/dist/commands/test-claude.d.ts +2 -2
- package/dist/commands/test-claude.js +3 -3
- package/dist/deploy/index.d.ts +0 -2
- package/dist/deploy/index.js +0 -2
- package/dist/deploy/settings.d.ts +6 -5
- package/dist/deploy/settings.js +64 -16
- package/dist/deploy/skills.js +1 -2
- package/dist/index.js +86 -96
- package/dist/lib/usage-parser.d.ts +1 -1
- package/dist/lib/usage-parser.js +9 -6
- package/dist/local/index.d.ts +14 -0
- package/dist/local/index.js +28 -0
- package/dist/local/local-embeddings.d.ts +49 -0
- package/dist/local/local-embeddings.js +232 -0
- package/dist/local/offline-fallback.d.ts +44 -0
- package/dist/local/offline-fallback.js +159 -0
- package/dist/local/sqlite-store.d.ts +126 -0
- package/dist/local/sqlite-store.js +393 -0
- package/dist/local/sync-engine.d.ts +42 -0
- package/dist/local/sync-engine.js +223 -0
- package/dist/utils/platform.d.ts +5 -1
- package/dist/utils/platform.js +24 -4
- package/dist/utils/proxy-url.d.ts +21 -0
- package/dist/utils/proxy-url.js +34 -0
- package/dist/utils/state.d.ts +1 -1
- package/dist/utils/state.js +11 -3
- package/dist/utils/templates.js +1 -1
- package/package.json +11 -4
- package/templates/CLAUDE.md +49 -107
- package/dist/agent/daemon.d.ts +0 -130
- package/dist/agent/daemon.js +0 -606
- package/dist/agent/health-check.d.ts +0 -35
- package/dist/agent/health-check.js +0 -243
- package/dist/agent/pty-runner.d.ts +0 -53
- package/dist/agent/pty-runner.js +0 -190
- package/dist/commands/agent.d.ts +0 -50
- package/dist/commands/agent.js +0 -544
- package/dist/commands/setup-remote.d.ts +0 -20
- package/dist/commands/setup-remote.js +0 -582
- package/dist/utils/verify-remote-terminal.d.ts +0 -10
- package/dist/utils/verify-remote-terminal.js +0 -415
- package/templates/README.md +0 -378
- package/templates/claude-plugins/PHASE2_COMPLETION.md +0 -346
- package/templates/claude-plugins/PLUGIN_PROPOSALS.md +0 -1776
- package/templates/claude-plugins/README.md +0 -587
- package/templates/claude-plugins/agents/code-reviewer.json +0 -14
- package/templates/claude-plugins/agents/debug-detective.json +0 -15
- package/templates/claude-plugins/agents/git-companion.json +0 -14
- package/templates/claude-plugins/blog-manager/.claude-plugin/plugin.json +0 -8
- package/templates/claude-plugins/blog-manager/commands/blog.md +0 -691
- package/templates/claude-plugins/golden-loop-monitor/.claude-plugin/plugin.json +0 -8
- package/templates/claude-plugins/golden-loop-monitor/commands/loop-status.md +0 -434
- package/templates/claude-plugins/learning-tracker/.claude-plugin/plugin.json +0 -8
- package/templates/claude-plugins/learning-tracker/commands/my-patterns.md +0 -282
- package/templates/claude-plugins/memory-lens/.claude-plugin/plugin.json +0 -8
- package/templates/claude-plugins/memory-lens/commands/memory-search.md +0 -181
- package/templates/claude-plugins/pattern-coach/.claude-plugin/plugin.json +0 -8
- package/templates/claude-plugins/pattern-coach/commands/forge.md +0 -365
- package/templates/claude-plugins/project-schema-validator/.claude-plugin/plugin.json +0 -8
- package/templates/claude-plugins/project-schema-validator/commands/validate-schema.md +0 -582
- package/templates/claude-plugins-admin/AGENT_TEAM_PROPOSALS.md +0 -819
- package/templates/claude-plugins-admin/README.md +0 -446
- package/templates/claude-plugins-admin/autonomous-admin-agent/.claude-plugin/plugin.json +0 -8
- package/templates/claude-plugins-admin/autonomous-admin-agent/commands/agent.md +0 -595
- package/templates/claude-plugins-admin/backend-agent/.claude-plugin/plugin.json +0 -8
- package/templates/claude-plugins-admin/backend-agent/commands/backend.md +0 -798
- package/templates/claude-plugins-admin/deploy-guardian/.claude-plugin/plugin.json +0 -8
- package/templates/claude-plugins-admin/deploy-guardian/commands/deploy.md +0 -554
- package/templates/claude-plugins-admin/frontend-agent/.claude-plugin/plugin.json +0 -8
- package/templates/claude-plugins-admin/frontend-agent/commands/frontend.md +0 -881
- package/templates/claude-plugins-admin/mcp-server-manager/.claude-plugin/plugin.json +0 -8
- package/templates/claude-plugins-admin/mcp-server-manager/commands/mcp.md +0 -85
- package/templates/claude-plugins-admin/memory-system-monitor/.claude-plugin/plugin.json +0 -8
- package/templates/claude-plugins-admin/memory-system-monitor/commands/memory-health.md +0 -569
- package/templates/claude-plugins-admin/qa-agent/.claude-plugin/plugin.json +0 -8
- package/templates/claude-plugins-admin/qa-agent/commands/qa.md +0 -863
- package/templates/claude-plugins-admin/tech-lead-agent/.claude-plugin/plugin.json +0 -8
- package/templates/claude-plugins-admin/tech-lead-agent/commands/lead.md +0 -732
- package/templates/commands/continue.md +0 -47
- package/templates/cursor-rules/ekkos-memory.md +0 -127
- package/templates/ekkos-manifest.json +0 -223
- package/templates/helpers/json-parse.cjs +0 -101
- package/templates/hooks-node/lib/state.js +0 -187
- package/templates/hooks-node/stop.js +0 -416
- package/templates/hooks-node/user-prompt-submit.js +0 -337
- package/templates/plan-template.md +0 -306
- package/templates/rules/00-hooks-contract.mdc +0 -89
- package/templates/rules/30-ekkos-core.mdc +0 -188
- package/templates/rules/31-ekkos-messages.mdc +0 -78
- package/templates/shared/hooks-enabled.json +0 -22
- package/templates/shared/session-words.json +0 -45
- package/templates/skills/ekkOS_Deep_Recall/Skill.md +0 -282
- package/templates/skills/ekkOS_Learn/Skill.md +0 -265
- package/templates/skills/ekkOS_Memory_First/Skill.md +0 -206
- package/templates/skills/ekkOS_Plan_Assist/Skill.md +0 -302
- package/templates/skills/ekkOS_Preferences/Skill.md +0 -247
- package/templates/skills/ekkOS_Reflect/Skill.md +0 -257
- package/templates/skills/ekkOS_Safety/Skill.md +0 -265
- package/templates/skills/ekkOS_Schema/Skill.md +0 -251
- package/templates/skills/ekkOS_Summary/Skill.md +0 -257
- package/templates/spec-template.md +0 -159
- package/templates/windsurf-rules/ekkos-memory.md +0 -127
- package/templates/windsurf-skills/README.md +0 -58
- package/templates/windsurf-skills/ekkos-continue/SKILL.md +0 -81
- package/templates/windsurf-skills/ekkos-golden-loop/SKILL.md +0 -225
- package/templates/windsurf-skills/ekkos-insights/SKILL.md +0 -138
- package/templates/windsurf-skills/ekkos-recall/SKILL.md +0 -96
- package/templates/windsurf-skills/ekkos-safety/SKILL.md +0 -89
- package/templates/windsurf-skills/ekkos-vault/SKILL.md +0 -86
package/dist/agent/daemon.js
DELETED
|
@@ -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 {};
|