@cmdctrl/aider 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/adapter/agentapi.d.ts +100 -0
  2. package/dist/adapter/agentapi.d.ts.map +1 -0
  3. package/dist/adapter/agentapi.js +578 -0
  4. package/dist/adapter/agentapi.js.map +1 -0
  5. package/dist/client/messages.d.ts +89 -0
  6. package/dist/client/messages.d.ts.map +1 -0
  7. package/dist/client/messages.js +6 -0
  8. package/dist/client/messages.js.map +1 -0
  9. package/dist/client/websocket.d.ts +66 -0
  10. package/dist/client/websocket.d.ts.map +1 -0
  11. package/dist/client/websocket.js +276 -0
  12. package/dist/client/websocket.js.map +1 -0
  13. package/dist/commands/register.d.ts +10 -0
  14. package/dist/commands/register.d.ts.map +1 -0
  15. package/dist/commands/register.js +175 -0
  16. package/dist/commands/register.js.map +1 -0
  17. package/dist/commands/start.d.ts +9 -0
  18. package/dist/commands/start.d.ts.map +1 -0
  19. package/dist/commands/start.js +54 -0
  20. package/dist/commands/start.js.map +1 -0
  21. package/dist/commands/status.d.ts +5 -0
  22. package/dist/commands/status.d.ts.map +1 -0
  23. package/dist/commands/status.js +37 -0
  24. package/dist/commands/status.js.map +1 -0
  25. package/dist/commands/stop.d.ts +5 -0
  26. package/dist/commands/stop.d.ts.map +1 -0
  27. package/dist/commands/stop.js +59 -0
  28. package/dist/commands/stop.js.map +1 -0
  29. package/dist/config/config.d.ts +60 -0
  30. package/dist/config/config.d.ts.map +1 -0
  31. package/dist/config/config.js +176 -0
  32. package/dist/config/config.js.map +1 -0
  33. package/dist/index.d.ts +3 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +34 -0
  36. package/dist/index.js.map +1 -0
  37. package/package.json +42 -0
  38. package/src/adapter/agentapi.ts +656 -0
  39. package/src/client/messages.ts +125 -0
  40. package/src/client/websocket.ts +317 -0
  41. package/src/commands/register.ts +201 -0
  42. package/src/commands/start.ts +70 -0
  43. package/src/commands/status.ts +45 -0
  44. package/src/commands/stop.ts +58 -0
  45. package/src/config/config.ts +146 -0
  46. package/src/index.ts +39 -0
  47. package/tsconfig.json +19 -0
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Message types for daemon <-> server communication
3
+ */
4
+
5
+ // Server -> Daemon messages
6
+
7
+ export interface PingMessage {
8
+ type: 'ping';
9
+ }
10
+
11
+ export interface TaskStartMessage {
12
+ type: 'task_start';
13
+ task_id: string;
14
+ instruction: string;
15
+ project_path?: string;
16
+ }
17
+
18
+ export interface TaskResumeMessage {
19
+ type: 'task_resume';
20
+ task_id: string;
21
+ session_id: string;
22
+ message: string;
23
+ project_path?: string;
24
+ }
25
+
26
+ export interface TaskCancelMessage {
27
+ type: 'task_cancel';
28
+ task_id: string;
29
+ }
30
+
31
+ export interface GetMessagesMessage {
32
+ type: 'get_messages';
33
+ request_id: string;
34
+ session_id: string;
35
+ limit: number;
36
+ before_uuid?: string;
37
+ after_uuid?: string;
38
+ }
39
+
40
+ // Watch/unwatch are Claude Code specific (JSONL file watching)
41
+ // Aider ignores these but we define them for type safety
42
+ export interface WatchSessionMessage {
43
+ type: 'watch_session';
44
+ session_id: string;
45
+ file_path: string;
46
+ }
47
+
48
+ export interface UnwatchSessionMessage {
49
+ type: 'unwatch_session';
50
+ session_id: string;
51
+ }
52
+
53
+ export type ServerMessage =
54
+ | PingMessage
55
+ | TaskStartMessage
56
+ | TaskResumeMessage
57
+ | TaskCancelMessage
58
+ | GetMessagesMessage
59
+ | WatchSessionMessage
60
+ | UnwatchSessionMessage;
61
+
62
+ // Daemon -> Server messages
63
+
64
+ export interface PongMessage {
65
+ type: 'pong';
66
+ }
67
+
68
+ export interface StatusMessage {
69
+ type: 'status';
70
+ running_tasks: string[];
71
+ }
72
+
73
+ export interface EventMessage {
74
+ type: 'event';
75
+ task_id: string;
76
+ event_type: string;
77
+ [key: string]: unknown;
78
+ }
79
+
80
+ export interface SessionInfo {
81
+ session_id: string;
82
+ slug: string;
83
+ title: string;
84
+ project: string;
85
+ project_name: string;
86
+ file_path: string;
87
+ last_message: string;
88
+ last_activity: string;
89
+ is_active: boolean;
90
+ message_count: number;
91
+ }
92
+
93
+ export interface ReportSessionsMessage {
94
+ type: 'report_sessions';
95
+ sessions: SessionInfo[];
96
+ }
97
+
98
+ export interface MessageEntry {
99
+ uuid: string;
100
+ role: 'USER' | 'AGENT' | 'SYSTEM';
101
+ content: string;
102
+ timestamp: string;
103
+ }
104
+
105
+ export interface MessagesResponseMessage {
106
+ type: 'messages';
107
+ request_id: string;
108
+ session_id: string;
109
+ messages: MessageEntry[];
110
+ has_more: boolean;
111
+ oldest_uuid?: string;
112
+ newest_uuid?: string;
113
+ error?: string;
114
+ }
115
+
116
+ export type DaemonMessage = PongMessage | StatusMessage | EventMessage | ReportSessionsMessage | MessagesResponseMessage;
117
+
118
+ // Event types sent from daemon to server
119
+ export type EventType =
120
+ | 'WAIT_FOR_USER'
121
+ | 'TASK_COMPLETE'
122
+ | 'OUTPUT'
123
+ | 'PROGRESS'
124
+ | 'WARNING'
125
+ | 'ERROR';
@@ -0,0 +1,317 @@
1
+ import WebSocket from 'ws';
2
+ import { URL } from 'url';
3
+ import { CmdCtrlConfig, Credentials, writeCredentials } from '../config/config';
4
+ import { AiderAdapter } from '../adapter/agentapi';
5
+ import {
6
+ ServerMessage,
7
+ DaemonMessage,
8
+ TaskStartMessage,
9
+ TaskResumeMessage,
10
+ TaskCancelMessage,
11
+ GetMessagesMessage
12
+ } from './messages';
13
+
14
+ const MAX_RECONNECT_DELAY = 30000; // 30 seconds
15
+ const INITIAL_RECONNECT_DELAY = 1000; // 1 second
16
+ const PING_INTERVAL = 30000; // 30 seconds
17
+
18
+ export class DaemonClient {
19
+ private ws: WebSocket | null = null;
20
+ private config: CmdCtrlConfig;
21
+ private credentials: Credentials;
22
+ private reconnectDelay = INITIAL_RECONNECT_DELAY;
23
+ private reconnectTimer: NodeJS.Timeout | null = null;
24
+ private pingTimer: NodeJS.Timeout | null = null;
25
+ private shouldReconnect = true;
26
+ private adapter: AiderAdapter;
27
+
28
+ constructor(config: CmdCtrlConfig, credentials: Credentials) {
29
+ this.config = config;
30
+ this.credentials = credentials;
31
+ this.adapter = new AiderAdapter(this.sendEvent.bind(this));
32
+ }
33
+
34
+ /**
35
+ * Connect to the CmdCtrl server via WebSocket
36
+ */
37
+ async connect(): Promise<void> {
38
+ return new Promise((resolve, reject) => {
39
+ const serverUrl = new URL(this.config.serverUrl);
40
+ const wsProtocol = serverUrl.protocol === 'https:' ? 'wss:' : 'ws:';
41
+ const wsUrl = `${wsProtocol}//${serverUrl.host}/ws/daemon`;
42
+
43
+ console.log(`Connecting to ${wsUrl}...`);
44
+
45
+ this.ws = new WebSocket(wsUrl, {
46
+ headers: {
47
+ Authorization: `Bearer ${this.credentials.refreshToken}`,
48
+ 'X-Device-ID': this.config.deviceId,
49
+ 'X-Agent-Type': 'aider'
50
+ }
51
+ });
52
+
53
+ this.ws.on('open', () => {
54
+ console.log('WebSocket connected');
55
+ this.reconnectDelay = INITIAL_RECONNECT_DELAY;
56
+ this.startPingInterval();
57
+ this.sendStatus();
58
+ resolve();
59
+ });
60
+
61
+ this.ws.on('message', (data) => {
62
+ this.handleMessage(data.toString());
63
+ });
64
+
65
+ this.ws.on('close', (code, reason) => {
66
+ console.log(`WebSocket closed: ${code} ${reason}`);
67
+ this.stopPingInterval();
68
+ this.scheduleReconnect();
69
+ });
70
+
71
+ this.ws.on('error', (err) => {
72
+ console.error('WebSocket error:', err.message);
73
+ if (this.ws?.readyState === WebSocket.CONNECTING) {
74
+ reject(err);
75
+ }
76
+ // For established connections, the 'close' event usually follows 'error',
77
+ // but if the connection was killed abruptly, we may not get a clean close.
78
+ // Force close the socket to ensure 'close' event fires and triggers reconnect.
79
+ if (this.ws?.readyState === WebSocket.OPEN) {
80
+ this.ws.terminate();
81
+ }
82
+ });
83
+ });
84
+ }
85
+
86
+ /**
87
+ * Disconnect from server
88
+ */
89
+ async disconnect(): Promise<void> {
90
+ this.shouldReconnect = false;
91
+
92
+ if (this.reconnectTimer) {
93
+ clearTimeout(this.reconnectTimer);
94
+ this.reconnectTimer = null;
95
+ }
96
+
97
+ this.stopPingInterval();
98
+
99
+ // Stop all running tasks
100
+ await this.adapter.stopAll();
101
+
102
+ if (this.ws) {
103
+ this.ws.close(1000, 'Daemon shutting down');
104
+ this.ws = null;
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Send a message to the server
110
+ */
111
+ private send(message: DaemonMessage): void {
112
+ if (this.ws?.readyState === WebSocket.OPEN) {
113
+ const json = JSON.stringify(message);
114
+ if (message.type !== 'pong') {
115
+ console.log(`[WS OUT] ${message.type}:`, json.length > 200 ? json.substring(0, 200) + '...' : json);
116
+ }
117
+ this.ws.send(json);
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Send an event for a task
123
+ */
124
+ private sendEvent(
125
+ taskId: string,
126
+ eventType: string,
127
+ data: Record<string, unknown>
128
+ ): void {
129
+ this.send({
130
+ type: 'event',
131
+ task_id: taskId,
132
+ event_type: eventType,
133
+ ...data
134
+ });
135
+ }
136
+
137
+ /**
138
+ * Send current status to server
139
+ */
140
+ private sendStatus(): void {
141
+ this.send({
142
+ type: 'status',
143
+ running_tasks: this.adapter.getRunningTasks()
144
+ });
145
+ }
146
+
147
+ /**
148
+ * Handle incoming message from server
149
+ */
150
+ private handleMessage(data: string): void {
151
+ try {
152
+ const msg = JSON.parse(data) as ServerMessage;
153
+
154
+ switch (msg.type) {
155
+ case 'ping':
156
+ this.send({ type: 'pong' });
157
+ break;
158
+
159
+ case 'task_start':
160
+ this.handleTaskStart(msg as TaskStartMessage);
161
+ break;
162
+
163
+ case 'task_resume':
164
+ this.handleTaskResume(msg as TaskResumeMessage);
165
+ break;
166
+
167
+ case 'task_cancel':
168
+ this.handleTaskCancel(msg as TaskCancelMessage);
169
+ break;
170
+
171
+ case 'get_messages':
172
+ this.handleGetMessages(msg as GetMessagesMessage);
173
+ break;
174
+
175
+ case 'watch_session':
176
+ case 'unwatch_session':
177
+ // Aider doesn't support file watching (Claude Code specific)
178
+ // Silently ignore these messages
179
+ break;
180
+
181
+ default:
182
+ console.log(`Unknown message type: ${(msg as { type: string }).type}`);
183
+ }
184
+ } catch (err) {
185
+ console.error('Failed to parse message:', err);
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Handle task_start message
191
+ */
192
+ private async handleTaskStart(msg: TaskStartMessage): Promise<void> {
193
+ console.log(`Received task_start: ${msg.task_id}`);
194
+ try {
195
+ await this.adapter.startTask(msg.task_id, msg.instruction, msg.project_path);
196
+ } catch (err) {
197
+ console.error(`Failed to start task ${msg.task_id}:`, err);
198
+ this.sendEvent(msg.task_id, 'ERROR', {
199
+ error: (err as Error).message
200
+ });
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Handle task_resume message
206
+ */
207
+ private async handleTaskResume(msg: TaskResumeMessage): Promise<void> {
208
+ console.log(`Received task_resume: ${msg.task_id}`);
209
+ try {
210
+ await this.adapter.resumeTask(msg.task_id, msg.session_id, msg.message, msg.project_path);
211
+ } catch (err) {
212
+ console.error(`Failed to resume task ${msg.task_id}:`, err);
213
+ this.sendEvent(msg.task_id, 'ERROR', {
214
+ error: (err as Error).message
215
+ });
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Handle task_cancel message
221
+ */
222
+ private async handleTaskCancel(msg: TaskCancelMessage): Promise<void> {
223
+ console.log(`Received task_cancel: ${msg.task_id}`);
224
+ await this.adapter.cancelTask(msg.task_id);
225
+ }
226
+
227
+ /**
228
+ * Handle get_messages request
229
+ * Fetches messages from AgentAPI's /messages endpoint
230
+ */
231
+ private async handleGetMessages(msg: GetMessagesMessage): Promise<void> {
232
+ console.log(`Received get_messages for session: ${msg.session_id}, after=${msg.after_uuid || 'none'}`);
233
+
234
+ // The session_id is the canonical ID like "dev-xxx:aider:PENDING-xxx"
235
+ // We need to find the task_id which matches this session
236
+ const taskId = msg.session_id;
237
+
238
+ const messages = await this.adapter.getMessages(taskId);
239
+
240
+ if (messages === null) {
241
+ // AgentAPI not running for this session
242
+ this.send({
243
+ type: 'messages',
244
+ request_id: msg.request_id,
245
+ session_id: msg.session_id,
246
+ messages: [],
247
+ has_more: false,
248
+ error: 'Session not active - AgentAPI not running'
249
+ });
250
+ return;
251
+ }
252
+
253
+ // Convert AgentAPI messages to our format
254
+ let formattedMessages = messages.map(m => ({
255
+ uuid: `aider-msg-${m.id}`,
256
+ role: m.role === 'agent' ? 'AGENT' as const : 'USER' as const,
257
+ content: m.content,
258
+ timestamp: m.time
259
+ }));
260
+
261
+ // Handle after_uuid for incremental fetches
262
+ if (msg.after_uuid) {
263
+ const afterIdx = formattedMessages.findIndex(m => m.uuid === msg.after_uuid);
264
+ if (afterIdx !== -1) {
265
+ formattedMessages = formattedMessages.slice(afterIdx + 1);
266
+ }
267
+ }
268
+
269
+ this.send({
270
+ type: 'messages',
271
+ request_id: msg.request_id,
272
+ session_id: msg.session_id,
273
+ messages: formattedMessages,
274
+ has_more: false,
275
+ newest_uuid: formattedMessages.length > 0 ? formattedMessages[formattedMessages.length - 1].uuid : undefined,
276
+ });
277
+ }
278
+
279
+ /**
280
+ * Schedule reconnection with exponential backoff
281
+ */
282
+ private scheduleReconnect(): void {
283
+ if (!this.shouldReconnect) {
284
+ return;
285
+ }
286
+
287
+ console.log(`Reconnecting in ${this.reconnectDelay}ms...`);
288
+ this.reconnectTimer = setTimeout(async () => {
289
+ try {
290
+ await this.connect();
291
+ } catch (err) {
292
+ console.error('Reconnection failed:', err);
293
+ this.reconnectDelay = Math.min(this.reconnectDelay * 2, MAX_RECONNECT_DELAY);
294
+ this.scheduleReconnect();
295
+ }
296
+ }, this.reconnectDelay);
297
+ }
298
+
299
+ /**
300
+ * Start ping interval to keep connection alive
301
+ */
302
+ private startPingInterval(): void {
303
+ this.pingTimer = setInterval(() => {
304
+ this.sendStatus();
305
+ }, PING_INTERVAL);
306
+ }
307
+
308
+ /**
309
+ * Stop ping interval
310
+ */
311
+ private stopPingInterval(): void {
312
+ if (this.pingTimer) {
313
+ clearInterval(this.pingTimer);
314
+ this.pingTimer = null;
315
+ }
316
+ }
317
+ }
@@ -0,0 +1,201 @@
1
+ import * as os from 'os';
2
+ import * as http from 'http';
3
+ import * as https from 'https';
4
+ import { URL } from 'url';
5
+ import {
6
+ writeConfig,
7
+ writeCredentials,
8
+ readConfig,
9
+ isRegistered,
10
+ CmdCtrlConfig,
11
+ Credentials
12
+ } from '../config/config';
13
+
14
+ interface RegisterOptions {
15
+ server: string;
16
+ name?: string;
17
+ }
18
+
19
+ interface DeviceCodeResponse {
20
+ deviceCode: string;
21
+ userCode: string;
22
+ verificationUrl: string;
23
+ expiresIn: number;
24
+ interval: number;
25
+ }
26
+
27
+ interface TokenResponse {
28
+ accessToken: string;
29
+ refreshToken: string;
30
+ expiresIn: number;
31
+ deviceId: string;
32
+ }
33
+
34
+ /**
35
+ * Make an HTTP(S) request
36
+ */
37
+ function request(
38
+ url: string,
39
+ method: string,
40
+ body?: object
41
+ ): Promise<{ status: number; data: unknown }> {
42
+ return new Promise((resolve, reject) => {
43
+ const parsed = new URL(url);
44
+ const client = parsed.protocol === 'https:' ? https : http;
45
+
46
+ const options = {
47
+ hostname: parsed.hostname,
48
+ port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
49
+ path: parsed.pathname + parsed.search,
50
+ method,
51
+ headers: {
52
+ 'Content-Type': 'application/json',
53
+ Accept: 'application/json'
54
+ }
55
+ };
56
+
57
+ const req = client.request(options, (res) => {
58
+ let data = '';
59
+ res.on('data', (chunk) => (data += chunk));
60
+ res.on('end', () => {
61
+ try {
62
+ const parsed = data ? JSON.parse(data) : {};
63
+ resolve({ status: res.statusCode || 0, data: parsed });
64
+ } catch {
65
+ resolve({ status: res.statusCode || 0, data: {} });
66
+ }
67
+ });
68
+ });
69
+
70
+ req.on('error', reject);
71
+
72
+ if (body) {
73
+ req.write(JSON.stringify(body));
74
+ }
75
+ req.end();
76
+ });
77
+ }
78
+
79
+ /**
80
+ * Poll for token completion (after user verifies in browser)
81
+ */
82
+ async function pollForToken(
83
+ serverUrl: string,
84
+ deviceCode: string,
85
+ interval: number,
86
+ expiresIn: number
87
+ ): Promise<TokenResponse | null> {
88
+ const startTime = Date.now();
89
+ const expiresAt = startTime + expiresIn * 1000;
90
+
91
+ while (Date.now() < expiresAt) {
92
+ await new Promise((resolve) => setTimeout(resolve, interval * 1000));
93
+
94
+ try {
95
+ const response = await request(`${serverUrl}/api/devices/token`, 'POST', {
96
+ deviceCode
97
+ });
98
+
99
+ if (response.status === 200) {
100
+ return response.data as TokenResponse;
101
+ }
102
+
103
+ // 400 with "authorization_pending" means keep polling
104
+ const data = response.data as { error?: string };
105
+ if (response.status === 400 && data.error === 'authorization_pending') {
106
+ process.stdout.write('.');
107
+ continue;
108
+ }
109
+
110
+ // Other errors should stop polling
111
+ if (response.status >= 400) {
112
+ console.error('\nError polling for token:', data);
113
+ return null;
114
+ }
115
+ } catch (err) {
116
+ console.error('\nError polling for token:', err);
117
+ return null;
118
+ }
119
+ }
120
+
121
+ console.error('\nDevice code expired. Please try again.');
122
+ return null;
123
+ }
124
+
125
+ /**
126
+ * Register command - implements GitHub CLI style device auth flow
127
+ */
128
+ export async function register(options: RegisterOptions): Promise<void> {
129
+ const serverUrl = options.server.replace(/\/$/, '');
130
+ const deviceName = options.name || `${os.hostname()}-aider`;
131
+
132
+ // Check if already registered
133
+ if (isRegistered()) {
134
+ const config = readConfig();
135
+ console.log(`Already registered as "${config?.deviceName}" (${config?.deviceId})`);
136
+ console.log(`Server: ${config?.serverUrl}`);
137
+ console.log('\nTo re-register, delete ~/.cmdctrl-aider/config.json and ~/.cmdctrl-aider/credentials');
138
+ return;
139
+ }
140
+
141
+ console.log(`Registering Aider device "${deviceName}" with ${serverUrl}...\n`);
142
+
143
+ // Step 1: Request device code
144
+ let codeResponse: DeviceCodeResponse;
145
+ try {
146
+ const response = await request(`${serverUrl}/api/devices/code`, 'POST', {
147
+ deviceName,
148
+ hostname: os.hostname(),
149
+ agentType: 'aider'
150
+ });
151
+
152
+ if (response.status !== 200) {
153
+ console.error('Failed to get device code:', response.data);
154
+ process.exit(1);
155
+ }
156
+
157
+ codeResponse = response.data as DeviceCodeResponse;
158
+ } catch (err) {
159
+ console.error('Failed to connect to server:', err);
160
+ process.exit(1);
161
+ }
162
+
163
+ // Step 2: Display instructions to user
164
+ console.log('To complete registration, open this URL in your browser:\n');
165
+ console.log(` ${codeResponse.verificationUrl}\n`);
166
+ console.log('Then enter this code:\n');
167
+ console.log(` ${codeResponse.userCode}\n`);
168
+ console.log('Waiting for verification...');
169
+
170
+ // Step 3: Poll for completion
171
+ const tokenResponse = await pollForToken(
172
+ serverUrl,
173
+ codeResponse.deviceCode,
174
+ codeResponse.interval,
175
+ codeResponse.expiresIn
176
+ );
177
+
178
+ if (!tokenResponse) {
179
+ process.exit(1);
180
+ }
181
+
182
+ // Step 4: Save config and credentials
183
+ const config: CmdCtrlConfig = {
184
+ serverUrl,
185
+ deviceId: tokenResponse.deviceId,
186
+ deviceName
187
+ };
188
+
189
+ const credentials: Credentials = {
190
+ accessToken: tokenResponse.accessToken,
191
+ refreshToken: tokenResponse.refreshToken,
192
+ expiresAt: Date.now() + tokenResponse.expiresIn * 1000
193
+ };
194
+
195
+ writeConfig(config);
196
+ writeCredentials(credentials);
197
+
198
+ console.log('\n\nRegistration complete!');
199
+ console.log(`Device ID: ${tokenResponse.deviceId}`);
200
+ console.log(`\nRun 'cmdctrl-aider start' to connect to the server.`);
201
+ }
@@ -0,0 +1,70 @@
1
+ import {
2
+ readConfig,
3
+ readCredentials,
4
+ isRegistered,
5
+ writePidFile,
6
+ isDaemonRunning
7
+ } from '../config/config';
8
+ import { DaemonClient } from '../client/websocket';
9
+
10
+ interface StartOptions {
11
+ foreground?: boolean;
12
+ }
13
+
14
+ /**
15
+ * Start command - launch the daemon and connect to server
16
+ */
17
+ export async function start(options: StartOptions): Promise<void> {
18
+ // Check registration
19
+ if (!isRegistered()) {
20
+ console.error('Device not registered. Run "cmdctrl-aider register" first.');
21
+ process.exit(1);
22
+ }
23
+
24
+ // Check if already running
25
+ if (isDaemonRunning()) {
26
+ console.error('Daemon is already running. Run "cmdctrl-aider stop" first.');
27
+ process.exit(1);
28
+ }
29
+
30
+ const config = readConfig()!;
31
+ const credentials = readCredentials()!;
32
+
33
+ console.log(`Starting CmdCtrl Aider daemon...`);
34
+ console.log(`Server: ${config.serverUrl}`);
35
+ console.log(`Device: ${config.deviceName} (${config.deviceId})`);
36
+
37
+ // Write PID file
38
+ writePidFile(process.pid);
39
+
40
+ // Create and start client
41
+ const client = new DaemonClient(config, credentials);
42
+
43
+ // Handle shutdown signals
44
+ const shutdown = async () => {
45
+ console.log('\nShutting down...');
46
+ await client.disconnect();
47
+ process.exit(0);
48
+ };
49
+
50
+ process.on('SIGINT', shutdown);
51
+ process.on('SIGTERM', shutdown);
52
+
53
+ // Connect and run
54
+ try {
55
+ await client.connect();
56
+ console.log('Connected to server.');
57
+
58
+ if (options.foreground) {
59
+ console.log('Running in foreground. Press Ctrl+C to stop.\n');
60
+ }
61
+
62
+ // Keep process alive - the WebSocket client handles events
63
+ await new Promise(() => {
64
+ // Never resolves - daemon runs until killed
65
+ });
66
+ } catch (err) {
67
+ console.error('Failed to start daemon:', err);
68
+ process.exit(1);
69
+ }
70
+ }