@ekkos/cli 0.2.10 → 0.2.11

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.
@@ -0,0 +1,86 @@
1
+ /**
2
+ * ekkOS Agent Daemon
3
+ *
4
+ * Background daemon that:
5
+ * 1. Maintains WebSocket connection to cloud relay
6
+ * 2. Receives session start requests
7
+ * 3. Spawns `ekkos run -d` in PTY
8
+ * 4. Relays PTY I/O to cloud
9
+ */
10
+ interface DaemonConfig {
11
+ deviceToken: string;
12
+ deviceId: string;
13
+ deviceName: string;
14
+ verbose?: boolean;
15
+ }
16
+ export declare class AgentDaemon {
17
+ private config;
18
+ private ws;
19
+ private reconnectAttempt;
20
+ private heartbeatTimer;
21
+ private ptyRunner;
22
+ private currentSessionId;
23
+ private running;
24
+ constructor(config: DaemonConfig);
25
+ /**
26
+ * Start the daemon
27
+ */
28
+ start(): Promise<void>;
29
+ /**
30
+ * Stop the daemon
31
+ */
32
+ stop(): Promise<void>;
33
+ /**
34
+ * Connect to relay server
35
+ */
36
+ private connect;
37
+ /**
38
+ * Handle WebSocket open
39
+ */
40
+ private handleOpen;
41
+ /**
42
+ * Handle incoming message
43
+ */
44
+ private handleMessage;
45
+ /**
46
+ * Handle session start request
47
+ */
48
+ private handleSessionStart;
49
+ /**
50
+ * Handle session end request
51
+ */
52
+ private handleSessionEnd;
53
+ /**
54
+ * Handle input from browser
55
+ */
56
+ private handleInput;
57
+ /**
58
+ * Handle resize from browser
59
+ */
60
+ private handleResize;
61
+ /**
62
+ * Handle PTY exit
63
+ */
64
+ private handlePTYExit;
65
+ /**
66
+ * Send PTY output to server
67
+ */
68
+ private sendOutput;
69
+ /**
70
+ * Handle WebSocket close
71
+ */
72
+ private handleClose;
73
+ /**
74
+ * Handle WebSocket error
75
+ */
76
+ private handleError;
77
+ /**
78
+ * Send message to server
79
+ */
80
+ private sendMessage;
81
+ /**
82
+ * Log message
83
+ */
84
+ private log;
85
+ }
86
+ export {};
@@ -0,0 +1,297 @@
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 RELAY_URL = process.env.RELAY_WS_URL || 'wss://mcp.ekkos.dev';
55
+ const HEARTBEAT_INTERVAL = 30000; // 30 seconds
56
+ const RECONNECT_DELAYS = [1000, 2000, 4000, 8000, 16000, 32000, 60000]; // Exponential backoff
57
+ class AgentDaemon {
58
+ constructor(config) {
59
+ this.ws = null;
60
+ this.reconnectAttempt = 0;
61
+ this.heartbeatTimer = null;
62
+ this.ptyRunner = null;
63
+ this.currentSessionId = null;
64
+ this.running = false;
65
+ this.config = config;
66
+ }
67
+ /**
68
+ * Start the daemon
69
+ */
70
+ async start() {
71
+ this.running = true;
72
+ this.log('Starting ekkOS agent daemon...');
73
+ this.connect();
74
+ }
75
+ /**
76
+ * Stop the daemon
77
+ */
78
+ async stop() {
79
+ this.running = false;
80
+ // Stop heartbeat
81
+ if (this.heartbeatTimer) {
82
+ clearInterval(this.heartbeatTimer);
83
+ this.heartbeatTimer = null;
84
+ }
85
+ // Close PTY
86
+ if (this.ptyRunner) {
87
+ this.ptyRunner.kill();
88
+ this.ptyRunner = null;
89
+ }
90
+ // Close WebSocket
91
+ if (this.ws) {
92
+ this.ws.close();
93
+ this.ws = null;
94
+ }
95
+ this.log('Daemon stopped');
96
+ }
97
+ /**
98
+ * Connect to relay server
99
+ */
100
+ connect() {
101
+ if (!this.running)
102
+ return;
103
+ const url = `${RELAY_URL}/api/v1/relay/device`;
104
+ this.log(`Connecting to ${url}...`);
105
+ this.ws = new ws_1.default(url, {
106
+ headers: {
107
+ 'X-Device-Token': this.config.deviceToken,
108
+ },
109
+ });
110
+ this.ws.on('open', () => this.handleOpen());
111
+ this.ws.on('message', (data) => this.handleMessage(data));
112
+ this.ws.on('close', (code, reason) => this.handleClose(code, reason.toString()));
113
+ this.ws.on('error', (err) => this.handleError(err));
114
+ }
115
+ /**
116
+ * Handle WebSocket open
117
+ */
118
+ handleOpen() {
119
+ this.log('Connected to relay server');
120
+ this.reconnectAttempt = 0;
121
+ // Start heartbeat
122
+ this.heartbeatTimer = setInterval(() => {
123
+ this.sendMessage({ type: 'heartbeat' });
124
+ }, HEARTBEAT_INTERVAL);
125
+ }
126
+ /**
127
+ * Handle incoming message
128
+ */
129
+ handleMessage(rawData) {
130
+ let message;
131
+ try {
132
+ message = JSON.parse(rawData.toString());
133
+ }
134
+ catch (err) {
135
+ this.log('Invalid message from server:', err);
136
+ return;
137
+ }
138
+ switch (message.type) {
139
+ case 'connected':
140
+ this.log('Registered with relay');
141
+ break;
142
+ case 'session_start':
143
+ this.handleSessionStart(message.sessionId);
144
+ break;
145
+ case 'session_end':
146
+ this.handleSessionEnd(message.sessionId);
147
+ break;
148
+ case 'input':
149
+ this.handleInput(message.data || '');
150
+ break;
151
+ case 'resize':
152
+ this.handleResize(message.cols || 80, message.rows || 24);
153
+ break;
154
+ case 'error':
155
+ this.log('Server error:', message.error);
156
+ break;
157
+ }
158
+ }
159
+ /**
160
+ * Handle session start request
161
+ */
162
+ handleSessionStart(sessionId) {
163
+ this.log(`Session start request: ${sessionId}`);
164
+ // Kill existing session if any
165
+ if (this.ptyRunner) {
166
+ this.ptyRunner.kill();
167
+ this.ptyRunner = null;
168
+ }
169
+ this.currentSessionId = sessionId;
170
+ // Start PTY with ekkos run -d
171
+ this.ptyRunner = new pty_runner_1.PTYRunner({
172
+ command: 'ekkos',
173
+ args: ['run', '-d', '-b'], // -d for doctor check, -b for bypass permissions
174
+ onData: (data) => this.sendOutput(data),
175
+ onExit: (code) => this.handlePTYExit(code),
176
+ verbose: this.config.verbose,
177
+ });
178
+ this.ptyRunner.start();
179
+ // Notify server that PTY is ready
180
+ this.sendMessage({
181
+ type: 'ready',
182
+ sessionId,
183
+ });
184
+ }
185
+ /**
186
+ * Handle session end request
187
+ */
188
+ handleSessionEnd(sessionId) {
189
+ if (sessionId && sessionId !== this.currentSessionId) {
190
+ return; // Not our session
191
+ }
192
+ this.log('Session ended');
193
+ if (this.ptyRunner) {
194
+ this.ptyRunner.kill();
195
+ this.ptyRunner = null;
196
+ }
197
+ this.currentSessionId = null;
198
+ }
199
+ /**
200
+ * Handle input from browser
201
+ */
202
+ handleInput(data) {
203
+ if (this.ptyRunner) {
204
+ this.ptyRunner.write(data);
205
+ }
206
+ }
207
+ /**
208
+ * Handle resize from browser
209
+ */
210
+ handleResize(cols, rows) {
211
+ if (this.ptyRunner) {
212
+ this.ptyRunner.resize(cols, rows);
213
+ }
214
+ }
215
+ /**
216
+ * Handle PTY exit
217
+ */
218
+ handlePTYExit(code) {
219
+ this.log(`PTY exited with code ${code}`);
220
+ this.sendMessage({
221
+ type: 'session_end',
222
+ sessionId: this.currentSessionId || undefined,
223
+ });
224
+ this.ptyRunner = null;
225
+ this.currentSessionId = null;
226
+ }
227
+ /**
228
+ * Send PTY output to server
229
+ */
230
+ sendOutput(data) {
231
+ this.sendMessage({
232
+ type: 'output',
233
+ sessionId: this.currentSessionId || undefined,
234
+ data,
235
+ });
236
+ }
237
+ /**
238
+ * Handle WebSocket close
239
+ */
240
+ handleClose(code, reason) {
241
+ this.log(`Disconnected: ${code} ${reason}`);
242
+ // Stop heartbeat
243
+ if (this.heartbeatTimer) {
244
+ clearInterval(this.heartbeatTimer);
245
+ this.heartbeatTimer = null;
246
+ }
247
+ // Kill PTY if running
248
+ if (this.ptyRunner) {
249
+ this.ptyRunner.kill();
250
+ this.ptyRunner = null;
251
+ }
252
+ this.currentSessionId = null;
253
+ // Reconnect if still running
254
+ if (this.running) {
255
+ const delay = RECONNECT_DELAYS[Math.min(this.reconnectAttempt, RECONNECT_DELAYS.length - 1)];
256
+ this.reconnectAttempt++;
257
+ this.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempt})...`);
258
+ setTimeout(() => this.connect(), delay);
259
+ }
260
+ }
261
+ /**
262
+ * Handle WebSocket error
263
+ */
264
+ handleError(err) {
265
+ this.log('WebSocket error:', err.message);
266
+ }
267
+ /**
268
+ * Send message to server
269
+ */
270
+ sendMessage(message) {
271
+ if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
272
+ this.ws.send(JSON.stringify(message));
273
+ }
274
+ }
275
+ /**
276
+ * Log message
277
+ */
278
+ log(...args) {
279
+ const timestamp = new Date().toISOString();
280
+ const message = `[${timestamp}] ${args.map(String).join(' ')}`;
281
+ if (this.config.verbose) {
282
+ console.log(message);
283
+ }
284
+ // Also log to file
285
+ try {
286
+ const logDir = path.join(os.homedir(), '.ekkos');
287
+ if (!fs.existsSync(logDir)) {
288
+ fs.mkdirSync(logDir, { recursive: true });
289
+ }
290
+ fs.appendFileSync(path.join(logDir, 'agent.log'), message + '\n');
291
+ }
292
+ catch {
293
+ // Ignore log errors
294
+ }
295
+ }
296
+ }
297
+ exports.AgentDaemon = AgentDaemon;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * PTY Runner for ekkOS Agent
3
+ *
4
+ * Spawns `ekkos run -d` in a pseudoterminal and handles I/O.
5
+ * Based on the PTY patterns from packages/ekkos-cli/src/commands/run.ts
6
+ */
7
+ interface PTYRunnerConfig {
8
+ command: string;
9
+ args: string[];
10
+ onData: (data: string) => void;
11
+ onExit: (code: number) => void;
12
+ cols?: number;
13
+ rows?: number;
14
+ verbose?: boolean;
15
+ }
16
+ export declare class PTYRunner {
17
+ private config;
18
+ private ptyProcess;
19
+ private spawnProcess;
20
+ private usePty;
21
+ constructor(config: PTYRunnerConfig);
22
+ /**
23
+ * Start the PTY process
24
+ */
25
+ start(): void;
26
+ /**
27
+ * Start using node-pty (preferred)
28
+ */
29
+ private startWithPty;
30
+ /**
31
+ * Start using spawn (fallback for Windows without PTY)
32
+ */
33
+ private startWithSpawn;
34
+ /**
35
+ * Write data to PTY stdin
36
+ */
37
+ write(data: string): void;
38
+ /**
39
+ * Resize PTY
40
+ */
41
+ resize(cols: number, rows: number): void;
42
+ /**
43
+ * Kill the PTY process
44
+ */
45
+ kill(): void;
46
+ /**
47
+ * Check if PTY is running
48
+ */
49
+ isRunning(): boolean;
50
+ }
51
+ export {};
@@ -0,0 +1,184 @@
1
+ "use strict";
2
+ /**
3
+ * PTY Runner for ekkOS Agent
4
+ *
5
+ * Spawns `ekkos run -d` in a pseudoterminal and handles I/O.
6
+ * Based on the PTY patterns from packages/ekkos-cli/src/commands/run.ts
7
+ */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.PTYRunner = void 0;
43
+ const os = __importStar(require("os"));
44
+ const child_process_1 = require("child_process");
45
+ // Try to load node-pty
46
+ let pty = null;
47
+ try {
48
+ pty = require('node-pty');
49
+ }
50
+ catch {
51
+ // node-pty not available, will use spawn fallback
52
+ }
53
+ class PTYRunner {
54
+ constructor(config) {
55
+ this.ptyProcess = null;
56
+ this.spawnProcess = null;
57
+ this.config = config;
58
+ this.usePty = pty !== null;
59
+ }
60
+ /**
61
+ * Start the PTY process
62
+ */
63
+ start() {
64
+ if (this.usePty && pty) {
65
+ this.startWithPty();
66
+ }
67
+ else {
68
+ this.startWithSpawn();
69
+ }
70
+ }
71
+ /**
72
+ * Start using node-pty (preferred)
73
+ */
74
+ startWithPty() {
75
+ if (!pty)
76
+ return;
77
+ const shell = os.platform() === 'win32' ? 'cmd.exe' : 'bash';
78
+ const shellArgs = os.platform() === 'win32'
79
+ ? ['/c', this.config.command, ...this.config.args]
80
+ : ['-c', `${this.config.command} ${this.config.args.join(' ')}`];
81
+ this.ptyProcess = pty.spawn(shell, shellArgs, {
82
+ name: 'xterm-256color',
83
+ cols: this.config.cols || 80,
84
+ rows: this.config.rows || 24,
85
+ cwd: process.cwd(),
86
+ env: {
87
+ ...process.env,
88
+ TERM: 'xterm-256color',
89
+ COLORTERM: 'truecolor',
90
+ },
91
+ });
92
+ this.ptyProcess.onData((data) => {
93
+ this.config.onData(data);
94
+ });
95
+ this.ptyProcess.onExit(({ exitCode }) => {
96
+ this.config.onExit(exitCode);
97
+ });
98
+ }
99
+ /**
100
+ * Start using spawn (fallback for Windows without PTY)
101
+ */
102
+ startWithSpawn() {
103
+ const isWindows = os.platform() === 'win32';
104
+ if (isWindows) {
105
+ // Windows: Use cmd.exe
106
+ this.spawnProcess = (0, child_process_1.spawn)('cmd.exe', ['/c', this.config.command, ...this.config.args], {
107
+ stdio: ['pipe', 'pipe', 'pipe'],
108
+ cwd: process.cwd(),
109
+ env: process.env,
110
+ });
111
+ }
112
+ else {
113
+ // Unix: Use script for PTY emulation
114
+ const scriptArgs = os.platform() === 'darwin'
115
+ ? ['-q', '/dev/null', this.config.command, ...this.config.args]
116
+ : ['-q', '-c', `${this.config.command} ${this.config.args.join(' ')}`, '/dev/null'];
117
+ this.spawnProcess = (0, child_process_1.spawn)('script', scriptArgs, {
118
+ stdio: ['pipe', 'pipe', 'pipe'],
119
+ cwd: process.cwd(),
120
+ env: {
121
+ ...process.env,
122
+ TERM: 'xterm-256color',
123
+ },
124
+ });
125
+ }
126
+ if (this.spawnProcess.stdout) {
127
+ this.spawnProcess.stdout.on('data', (data) => {
128
+ this.config.onData(data.toString());
129
+ });
130
+ }
131
+ if (this.spawnProcess.stderr) {
132
+ this.spawnProcess.stderr.on('data', (data) => {
133
+ this.config.onData(data.toString());
134
+ });
135
+ }
136
+ this.spawnProcess.on('exit', (code) => {
137
+ this.config.onExit(code || 0);
138
+ });
139
+ this.spawnProcess.on('error', (err) => {
140
+ this.config.onData(`Error: ${err.message}\r\n`);
141
+ this.config.onExit(1);
142
+ });
143
+ }
144
+ /**
145
+ * Write data to PTY stdin
146
+ */
147
+ write(data) {
148
+ if (this.ptyProcess) {
149
+ this.ptyProcess.write(data);
150
+ }
151
+ else if (this.spawnProcess && this.spawnProcess.stdin) {
152
+ this.spawnProcess.stdin.write(data);
153
+ }
154
+ }
155
+ /**
156
+ * Resize PTY
157
+ */
158
+ resize(cols, rows) {
159
+ if (this.ptyProcess && this.ptyProcess.resize) {
160
+ this.ptyProcess.resize(cols, rows);
161
+ }
162
+ // Spawn fallback doesn't support resize
163
+ }
164
+ /**
165
+ * Kill the PTY process
166
+ */
167
+ kill() {
168
+ if (this.ptyProcess) {
169
+ this.ptyProcess.kill();
170
+ this.ptyProcess = null;
171
+ }
172
+ if (this.spawnProcess) {
173
+ this.spawnProcess.kill('SIGTERM');
174
+ this.spawnProcess = null;
175
+ }
176
+ }
177
+ /**
178
+ * Check if PTY is running
179
+ */
180
+ isRunning() {
181
+ return this.ptyProcess !== null || this.spawnProcess !== null;
182
+ }
183
+ }
184
+ exports.PTYRunner = PTYRunner;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * ekkos agent - Agent management commands for remote terminal
3
+ *
4
+ * Subcommands:
5
+ * - daemon: Run the agent daemon (used by service)
6
+ * - start: Start the agent service
7
+ * - stop: Stop the agent service
8
+ * - restart: Restart the agent service
9
+ * - status: Check agent status
10
+ * - uninstall: Remove agent service
11
+ */
12
+ export interface AgentOptions {
13
+ verbose?: boolean;
14
+ }
15
+ /**
16
+ * Run the agent daemon (foreground)
17
+ */
18
+ export declare function agentDaemon(options?: AgentOptions): Promise<void>;
19
+ /**
20
+ * Start the agent service
21
+ */
22
+ export declare function agentStart(options?: AgentOptions): Promise<void>;
23
+ /**
24
+ * Stop the agent service
25
+ */
26
+ export declare function agentStop(options?: AgentOptions): Promise<void>;
27
+ /**
28
+ * Restart the agent service
29
+ */
30
+ export declare function agentRestart(options?: AgentOptions): Promise<void>;
31
+ /**
32
+ * Check agent status
33
+ */
34
+ export declare function agentStatus(options?: AgentOptions): Promise<void>;
35
+ /**
36
+ * Uninstall the agent service
37
+ */
38
+ export declare function agentUninstall(options?: AgentOptions): Promise<void>;
39
+ /**
40
+ * Show agent logs
41
+ */
42
+ export declare function agentLogs(options?: {
43
+ follow?: boolean;
44
+ }): Promise<void>;