teleportation-cli 1.1.4 → 1.2.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 (48) hide show
  1. package/.claude/hooks/config-loader.mjs +88 -34
  2. package/.claude/hooks/permission_request.mjs +392 -82
  3. package/.claude/hooks/post_tool_use.mjs +90 -0
  4. package/.claude/hooks/pre_tool_use.mjs +247 -305
  5. package/.claude/hooks/session-register.mjs +94 -105
  6. package/.claude/hooks/session_end.mjs +41 -42
  7. package/.claude/hooks/session_start.mjs +45 -60
  8. package/.claude/hooks/stop.mjs +752 -99
  9. package/.claude/hooks/user_prompt_submit.mjs +26 -3
  10. package/README.md +7 -0
  11. package/lib/auth/api-key.js +12 -0
  12. package/lib/auth/token-refresh.js +286 -0
  13. package/lib/cli/daemon-commands.js +1 -1
  14. package/lib/cli/teleport-commands.js +469 -0
  15. package/lib/daemon/daemon-v2.js +104 -0
  16. package/lib/daemon/lifecycle.js +56 -171
  17. package/lib/daemon/response-classifier.js +15 -1
  18. package/lib/daemon/services/index.js +3 -0
  19. package/lib/daemon/services/polling-service.js +173 -0
  20. package/lib/daemon/services/queue-service.js +318 -0
  21. package/lib/daemon/services/session-service.js +115 -0
  22. package/lib/daemon/state.js +35 -0
  23. package/lib/daemon/task-executor-v2.js +413 -0
  24. package/lib/daemon/task-executor.js +1235 -0
  25. package/lib/daemon/teleportation-daemon.js +770 -25
  26. package/lib/daemon/timeline-analyzer.js +215 -0
  27. package/lib/daemon/transcript-ingestion.js +696 -0
  28. package/lib/daemon/utils.js +91 -0
  29. package/lib/install/installer.js +184 -20
  30. package/lib/install/uhr-installer.js +136 -0
  31. package/lib/remote/providers/base-provider.js +46 -0
  32. package/lib/remote/providers/daytona-provider.js +58 -0
  33. package/lib/remote/providers/provider-factory.js +90 -19
  34. package/lib/remote/providers/sprites-provider.js +711 -0
  35. package/lib/teleport/exporters/claude-exporter.js +302 -0
  36. package/lib/teleport/exporters/gemini-exporter.js +307 -0
  37. package/lib/teleport/exporters/index.js +93 -0
  38. package/lib/teleport/exporters/interface.js +153 -0
  39. package/lib/teleport/fork-tracker.js +415 -0
  40. package/lib/teleport/git-committer.js +337 -0
  41. package/lib/teleport/index.js +48 -0
  42. package/lib/teleport/manager.js +620 -0
  43. package/lib/teleport/session-capture.js +282 -0
  44. package/package.json +11 -5
  45. package/teleportation-cli.cjs +632 -451
  46. package/.claude/hooks/heartbeat.mjs +0 -396
  47. package/lib/daemon/agentic-executor.js +0 -803
  48. package/lib/daemon/pid-manager.js +0 -160
@@ -0,0 +1,469 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI Commands for Session Teleportation
4
+ *
5
+ * Provides command-line interface for:
6
+ * - Teleporting local sessions to cloud environments
7
+ * - Checking teleport status
8
+ * - Stopping teleports
9
+ * - Listing active teleports
10
+ */
11
+
12
+ import { TeleportManager, TeleportMode, TeleportStatus } from '../teleport/manager.js';
13
+ import { ProviderFactory } from '../remote/providers/provider-factory.js';
14
+ import { getSupportedCoders } from '../teleport/exporters/index.js';
15
+
16
+ /**
17
+ * Color helpers for terminal output
18
+ */
19
+ const c = {
20
+ red: (text) => `\x1b[0;31m${text}\x1b[0m`,
21
+ green: (text) => `\x1b[0;32m${text}\x1b[0m`,
22
+ yellow: (text) => `\x1b[1;33m${text}\x1b[0m`,
23
+ blue: (text) => `\x1b[0;34m${text}\x1b[0m`,
24
+ cyan: (text) => `\x1b[0;36m${text}\x1b[0m`,
25
+ purple: (text) => `\x1b[0;35m${text}\x1b[0m`,
26
+ dim: (text) => `\x1b[2m${text}\x1b[0m`,
27
+ };
28
+
29
+ /**
30
+ * Load saved relay credentials
31
+ */
32
+ async function loadSavedRelayCredentials() {
33
+ try {
34
+ const { CredentialManager } = await import('../auth/credentials.js');
35
+ const manager = new CredentialManager();
36
+ const creds = await manager.load();
37
+
38
+ return {
39
+ relayApiUrl: creds?.relayApiUrl || '',
40
+ relayApiKey: creds?.relayApiKey || creds?.apiKey || '',
41
+ githubToken: creds?.githubToken || '',
42
+ };
43
+ } catch (error) {
44
+ if (process.env.TELEPORTATION_DEBUG === 'true') {
45
+ console.error(`[teleport] Failed to load saved credentials: ${error.message}`);
46
+ }
47
+ return {
48
+ relayApiUrl: '',
49
+ relayApiKey: '',
50
+ githubToken: '',
51
+ };
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Create provider factory with environment variables
57
+ */
58
+ function createProviderFactory() {
59
+ return new ProviderFactory({
60
+ vaultClient: {
61
+ apiUrl: process.env.MECH_VAULT_URL || 'https://vault.mechdna.net/api',
62
+ apiKey: process.env.MECH_API_KEY,
63
+ appId: process.env.MECH_APP_ID,
64
+ },
65
+ livePortClient: {
66
+ apiKey: process.env.LIVEPORT_API_KEY,
67
+ apiUrl: process.env.LIVEPORT_API_URL || 'https://api.liveport.dev/v1',
68
+ },
69
+ flyApiToken: process.env.FLY_API_TOKEN,
70
+ daytonaApiKey: process.env.DAYTONA_API_KEY,
71
+ spritesApiKey: process.env.SPRITES_API_KEY,
72
+ });
73
+ }
74
+
75
+ /**
76
+ * Create TeleportManager instance
77
+ */
78
+ async function createTeleportManager() {
79
+ const savedCreds = await loadSavedRelayCredentials();
80
+
81
+ const relayApiUrl = process.env.RELAY_API_URL || savedCreds.relayApiUrl || 'https://api.teleportation.dev';
82
+ const relayApiKey = process.env.RELAY_API_KEY || savedCreds.relayApiKey;
83
+
84
+ if (!relayApiKey) {
85
+ throw new Error('No relay API key found. Run "teleportation login" first.');
86
+ }
87
+
88
+ const providerFactory = createProviderFactory();
89
+
90
+ return new TeleportManager({
91
+ providerFactory,
92
+ relayApiUrl,
93
+ relayApiKey,
94
+ });
95
+ }
96
+
97
+ /**
98
+ * Format status with color
99
+ */
100
+ function formatStatus(status) {
101
+ const statusColors = {
102
+ [TeleportStatus.PENDING]: c.dim,
103
+ [TeleportStatus.CAPTURING]: c.yellow,
104
+ [TeleportStatus.UPLOADING]: c.yellow,
105
+ [TeleportStatus.PROVISIONING]: c.yellow,
106
+ [TeleportStatus.EXPORTING]: c.yellow,
107
+ [TeleportStatus.RESUMING]: c.yellow,
108
+ [TeleportStatus.RUNNING]: c.green,
109
+ [TeleportStatus.COMPLETED]: c.blue,
110
+ [TeleportStatus.FAILED]: c.red,
111
+ };
112
+
113
+ const colorFn = statusColors[status] || c.dim;
114
+ return colorFn(status);
115
+ }
116
+
117
+ /**
118
+ * Format duration in milliseconds to human-readable string
119
+ */
120
+ function formatDuration(ms) {
121
+ const seconds = Math.floor(ms / 1000);
122
+ const minutes = Math.floor(seconds / 60);
123
+ const hours = Math.floor(minutes / 60);
124
+
125
+ if (hours > 0) return `${hours}h ${minutes % 60}m`;
126
+ if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
127
+ return `${seconds}s`;
128
+ }
129
+
130
+ /**
131
+ * Start a teleport session
132
+ *
133
+ * @param {Object} options
134
+ * @param {string} options.sessionId - Local session ID (or generates one)
135
+ * @param {string} [options.task] - Task description
136
+ * @param {string} [options.cwd] - Working directory
137
+ * @param {string} [options.provider] - Force specific provider
138
+ * @param {string} [options.targetCoder] - Target CLI (claude-code, gemini-cli)
139
+ * @param {string} [options.mode] - Teleport mode (pause, fork)
140
+ */
141
+ export async function commandTeleportStart(options = {}) {
142
+ console.log(c.purple('\nTeleporting Session to Cloud...\n'));
143
+
144
+ const {
145
+ sessionId = `local-${Date.now()}`,
146
+ task,
147
+ cwd = process.cwd(),
148
+ provider,
149
+ targetCoder = 'claude-code',
150
+ mode = 'pause',
151
+ } = options;
152
+
153
+ // Validate target coder
154
+ const supportedCoders = getSupportedCoders();
155
+ if (!supportedCoders.includes(targetCoder)) {
156
+ console.log(c.red(`❌ Unsupported coder: ${targetCoder}`));
157
+ console.log(c.cyan(`Supported coders: ${supportedCoders.join(', ')}\n`));
158
+ process.exit(1);
159
+ }
160
+
161
+ // Validate mode
162
+ if (!Object.values(TeleportMode).includes(mode)) {
163
+ console.log(c.red(`❌ Invalid mode: ${mode}`));
164
+ console.log(c.cyan(`Valid modes: ${Object.values(TeleportMode).join(', ')}\n`));
165
+ process.exit(1);
166
+ }
167
+
168
+ try {
169
+ const manager = await createTeleportManager();
170
+
171
+ // Progress callback
172
+ const onProgress = (event) => {
173
+ const statusIcon = {
174
+ [TeleportStatus.CAPTURING]: '📷',
175
+ [TeleportStatus.UPLOADING]: '⬆️',
176
+ [TeleportStatus.PROVISIONING]: '🚀',
177
+ [TeleportStatus.EXPORTING]: '📦',
178
+ [TeleportStatus.RESUMING]: '▶️',
179
+ [TeleportStatus.RUNNING]: '✅',
180
+ };
181
+ const icon = statusIcon[event.status] || '⏳';
182
+ console.log(` ${icon} ${event.status}`);
183
+ };
184
+
185
+ console.log(c.yellow('Session Details:'));
186
+ console.log(` Session ID: ${c.cyan(sessionId)}`);
187
+ console.log(` Working Dir: ${c.dim(cwd)}`);
188
+ console.log(` Target CLI: ${c.cyan(targetCoder)}`);
189
+ console.log(` Mode: ${c.cyan(mode)}`);
190
+ if (task) console.log(` Task: ${task}`);
191
+ if (provider) console.log(` Provider: ${c.cyan(provider)}`);
192
+ console.log('');
193
+
194
+ console.log(c.yellow('Progress:'));
195
+
196
+ const result = await manager.initiateTeleport(sessionId, {
197
+ task,
198
+ cwd,
199
+ provider,
200
+ targetCoder,
201
+ mode,
202
+ onProgress,
203
+ });
204
+
205
+ console.log('');
206
+ console.log(c.green('✅ Teleport Started Successfully!\n'));
207
+
208
+ console.log(c.yellow('Teleport Details:'));
209
+ console.log(` Teleport ID: ${c.cyan(result.teleportId)}`);
210
+ console.log(` Provider: ${c.cyan(result.provider)}`);
211
+ console.log(` Machine ID: ${c.dim(result.machineId)}`);
212
+ if (result.spriteUrl) {
213
+ console.log(` Sprite URL: ${c.blue(result.spriteUrl)}`);
214
+ }
215
+ if (result.tunnelUrl) {
216
+ console.log(` Tunnel URL: ${c.blue(result.tunnelUrl)}`);
217
+ }
218
+ console.log(` Timeline: ${c.blue(result.timelineUrl)}`);
219
+ console.log('');
220
+
221
+ if (mode === TeleportMode.PAUSE) {
222
+ console.log(c.yellow('⏸️ Local session paused. Work continues on cloud.\n'));
223
+ } else {
224
+ console.log(c.yellow('🔀 Fork mode: Local session continues in parallel.\n'));
225
+ }
226
+
227
+ return result;
228
+ } catch (error) {
229
+ console.log(c.red(`\n❌ Teleport failed: ${error.message}\n`));
230
+ if (process.env.TELEPORTATION_DEBUG === 'true') {
231
+ console.error(error.stack);
232
+ }
233
+ process.exit(1);
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Get teleport status
239
+ *
240
+ * @param {string} teleportId - Teleport ID
241
+ */
242
+ export async function commandTeleportStatus(teleportId) {
243
+ if (!teleportId) {
244
+ console.log(c.red('❌ Teleport ID is required\n'));
245
+ process.exit(1);
246
+ }
247
+
248
+ try {
249
+ const manager = await createTeleportManager();
250
+ const state = manager.getTeleportStatus(teleportId);
251
+
252
+ if (!state) {
253
+ console.log(c.red(`❌ Teleport not found: ${teleportId}\n`));
254
+ process.exit(1);
255
+ }
256
+
257
+ console.log(c.purple(`\nTeleport Status: ${teleportId}\n`));
258
+
259
+ console.log(c.yellow('Status:'));
260
+ console.log(` Status: ${formatStatus(state.status)}`);
261
+ console.log(` Session ID: ${c.cyan(state.sessionId)}`);
262
+ console.log(` Mode: ${c.cyan(state.mode)}`);
263
+ console.log(` Target CLI: ${c.cyan(state.targetCoder)}`);
264
+
265
+ if (state.provider) {
266
+ console.log(` Provider: ${c.cyan(state.provider)}`);
267
+ }
268
+ if (state.machineId) {
269
+ console.log(` Machine ID: ${c.dim(state.machineId)}`);
270
+ }
271
+
272
+ console.log('');
273
+ console.log(c.yellow('Timestamps:'));
274
+ console.log(` Created: ${c.dim(state.createdAt)}`);
275
+ if (state.updatedAt) {
276
+ console.log(` Updated: ${c.dim(state.updatedAt)}`);
277
+ }
278
+ if (state.completedAt) {
279
+ console.log(` Completed: ${c.dim(state.completedAt)}`);
280
+ }
281
+
282
+ if (state.task) {
283
+ console.log('');
284
+ console.log(c.yellow('Task:'));
285
+ console.log(` ${state.task}`);
286
+ }
287
+
288
+ if (state.error) {
289
+ console.log('');
290
+ console.log(c.red('Error:'));
291
+ console.log(` ${state.error}`);
292
+ }
293
+
294
+ // Get commit stats if available
295
+ const commitStats = manager.getCommitStats(teleportId);
296
+ if (commitStats) {
297
+ console.log('');
298
+ console.log(c.yellow('Git Commits:'));
299
+ console.log(` Total Commits: ${c.cyan(commitStats.commitCount)}`);
300
+ console.log(` Auto-Commit Active: ${commitStats.isRunning ? c.green('Yes') : c.dim('No')}`);
301
+ if (commitStats.lastCommitAt) {
302
+ console.log(` Last Commit: ${c.dim(commitStats.lastCommitAt)}`);
303
+ }
304
+ }
305
+
306
+ console.log('');
307
+
308
+ return state;
309
+ } catch (error) {
310
+ console.log(c.red(`\n❌ Failed to get status: ${error.message}\n`));
311
+ process.exit(1);
312
+ }
313
+ }
314
+
315
+ /**
316
+ * List active teleports
317
+ *
318
+ * @param {Object} [options]
319
+ * @param {string} [options.status] - Filter by status
320
+ */
321
+ export async function commandTeleportList(options = {}) {
322
+ console.log(c.purple('\nActive Teleports\n'));
323
+
324
+ try {
325
+ const manager = await createTeleportManager();
326
+ let teleports = manager.listActiveTeleports();
327
+
328
+ // Filter by status if specified
329
+ if (options.status) {
330
+ teleports = teleports.filter(t => t.status === options.status);
331
+ }
332
+
333
+ if (teleports.length === 0) {
334
+ console.log(c.dim('No active teleports found.\n'));
335
+ console.log(c.cyan('Start a teleport with: teleportation teleport start --task "your task"\n'));
336
+ return;
337
+ }
338
+
339
+ // Table header
340
+ console.log(c.dim('─'.repeat(80)));
341
+ console.log(
342
+ c.yellow('ID'.padEnd(20)) +
343
+ c.yellow('Status'.padEnd(15)) +
344
+ c.yellow('Provider'.padEnd(12)) +
345
+ c.yellow('CLI'.padEnd(15)) +
346
+ c.yellow('Age')
347
+ );
348
+ console.log(c.dim('─'.repeat(80)));
349
+
350
+ for (const t of teleports) {
351
+ const age = formatDuration(Date.now() - new Date(t.createdAt).getTime());
352
+ console.log(
353
+ t.teleportId.substring(0, 18).padEnd(20) +
354
+ formatStatus(t.status).padEnd(15 + 10) + // Extra for ANSI codes
355
+ (t.provider || '-').padEnd(12) +
356
+ t.targetCoder.padEnd(15) +
357
+ c.dim(age)
358
+ );
359
+ }
360
+
361
+ console.log(c.dim('─'.repeat(80)));
362
+ console.log(c.dim(`\nTotal: ${teleports.length} teleport(s)\n`));
363
+
364
+ return teleports;
365
+ } catch (error) {
366
+ console.log(c.red(`\n❌ Failed to list teleports: ${error.message}\n`));
367
+ process.exit(1);
368
+ }
369
+ }
370
+
371
+ /**
372
+ * Stop a teleport session
373
+ *
374
+ * @param {string} teleportId - Teleport ID
375
+ * @param {Object} [options]
376
+ * @param {boolean} [options.createPR] - Create PR with results
377
+ * @param {string} [options.baseBranch] - Base branch for PR
378
+ */
379
+ export async function commandTeleportStop(teleportId, options = {}) {
380
+ if (!teleportId) {
381
+ console.log(c.red('❌ Teleport ID is required\n'));
382
+ process.exit(1);
383
+ }
384
+
385
+ console.log(c.yellow(`\nStopping teleport: ${teleportId}\n`));
386
+
387
+ try {
388
+ const manager = await createTeleportManager();
389
+
390
+ const result = await manager.stopTeleport(teleportId, {
391
+ success: true,
392
+ createPR: options.createPR,
393
+ baseBranch: options.baseBranch,
394
+ });
395
+
396
+ if (!result.success) {
397
+ console.log(c.red(`❌ Failed to stop teleport: ${result.error}\n`));
398
+ process.exit(1);
399
+ }
400
+
401
+ console.log(c.green('✅ Teleport stopped successfully!\n'));
402
+
403
+ if (result.delivery) {
404
+ console.log(c.yellow('Results Delivered:'));
405
+ console.log(` Strategy: ${c.cyan(result.delivery.strategy)}`);
406
+ if (result.delivery.prUrl) {
407
+ console.log(` PR: ${c.blue(result.delivery.prUrl)}`);
408
+ }
409
+ if (result.delivery.branch) {
410
+ console.log(` Branch: ${c.cyan(result.delivery.branch)}`);
411
+ }
412
+ console.log('');
413
+ }
414
+
415
+ if (result.deliveryError) {
416
+ console.log(c.yellow('⚠️ Delivery issue:'));
417
+ console.log(` ${c.dim(result.deliveryError)}\n`);
418
+ }
419
+
420
+ return result;
421
+ } catch (error) {
422
+ console.log(c.red(`\n❌ Failed to stop teleport: ${error.message}\n`));
423
+ process.exit(1);
424
+ }
425
+ }
426
+
427
+ /**
428
+ * Show help for teleport commands
429
+ */
430
+ export function commandTeleportHelp() {
431
+ console.log(c.purple('\nSession Teleportation Commands\n'));
432
+ console.log(c.cyan('Transfer local coding sessions to cloud environments\n'));
433
+
434
+ console.log(c.yellow('Commands:'));
435
+ console.log(' ' + c.green('teleport start') + ' Start teleporting current session');
436
+ console.log(' ' + c.green('teleport status <id>') + ' Get teleport status');
437
+ console.log(' ' + c.green('teleport list') + ' List active teleports');
438
+ console.log(' ' + c.green('teleport stop <id>') + ' Stop a teleport session\n');
439
+
440
+ console.log(c.yellow('Start Options:'));
441
+ console.log(' ' + c.green('--task "description"') + ' Task for the cloud session');
442
+ console.log(' ' + c.green('--cwd /path') + ' Working directory (default: current)');
443
+ console.log(' ' + c.green('--provider sprites|daytona') + ' Force specific cloud provider');
444
+ console.log(' ' + c.green('--target-coder cli') + ' Target CLI (claude-code, gemini-cli)');
445
+ console.log(' ' + c.green('--mode pause|fork') + ' Teleport mode\n');
446
+
447
+ console.log(c.yellow('Stop Options:'));
448
+ console.log(' ' + c.green('--create-pr') + ' Create PR with results');
449
+ console.log(' ' + c.green('--base-branch main') + ' Base branch for PR\n');
450
+
451
+ console.log(c.yellow('Modes:'));
452
+ console.log(' ' + c.cyan('pause') + ' Pause local session, continue on cloud (default)');
453
+ console.log(' ' + c.cyan('fork') + ' Fork: local continues, cloud runs in parallel\n');
454
+
455
+ console.log(c.purple('Examples:'));
456
+ console.log(' teleportation teleport start --task "Implement feature X"');
457
+ console.log(' teleportation teleport start --target-coder gemini-cli --mode fork');
458
+ console.log(' teleportation teleport status tel_abc123xyz');
459
+ console.log(' teleportation teleport stop tel_abc123xyz --create-pr');
460
+ console.log('');
461
+ }
462
+
463
+ export default {
464
+ commandTeleportStart,
465
+ commandTeleportStatus,
466
+ commandTeleportList,
467
+ commandTeleportStop,
468
+ commandTeleportHelp,
469
+ };
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Teleportation Daemon v2
5
+ *
6
+ * Powered by @derivativelabs/agent-process.
7
+ * Decomposed into modular services.
8
+ */
9
+
10
+ import { createAgent } from '@derivativelabs/agent-process';
11
+ import { HeartbeatService } from '@derivativelabs/agent-process/services';
12
+ import { createDaemonState } from './state.js';
13
+ import { SessionService, QueueService, PollingService } from './services/index.js';
14
+ import { CredentialManager } from '../auth/credentials.js';
15
+
16
+ async function main() {
17
+ console.log('[daemon-v2] Starting up...');
18
+
19
+ // 1. Configuration
20
+ const config = {
21
+ port: parseInt(process.env.TELEPORTATION_DAEMON_PORT || '3050', 10),
22
+ relayApiUrl: process.env.RELAY_API_URL || 'https://api.teleportation.dev',
23
+ relayApiKey: process.env.RELAY_API_KEY || '',
24
+ pollIntervalMs: parseInt(process.env.DAEMON_POLL_INTERVAL_MS || '5000', 10),
25
+ cleanupIntervalMs: 60 * 60 * 1000, // 1 hour
26
+ idleCheckIntervalMs: 300000, // 5 min
27
+ idleTimeoutMs: 1800000, // 30 min
28
+ };
29
+
30
+ // 2. Load credentials if not in environment
31
+ if (!config.relayApiKey) {
32
+ try {
33
+ const credManager = new CredentialManager();
34
+ const creds = await credManager.load();
35
+ if (creds && creds.apiKey) {
36
+ config.relayApiKey = creds.apiKey;
37
+ config.relayApiUrl = creds.relayUrl || config.relayApiUrl;
38
+ }
39
+ } catch (e) {
40
+ console.warn('[daemon-v2] Failed to load credentials:', e.message);
41
+ }
42
+ }
43
+
44
+ // 3. Shared State
45
+ const state = createDaemonState();
46
+
47
+ // 4. Services
48
+ const sessionService = new SessionService(state, config);
49
+ const queueService = new QueueService(state, config);
50
+ const pollingService = new PollingService(state, config);
51
+
52
+ // 5. Agent
53
+ const agent = createAgent({
54
+ name: 'teleportation-daemon',
55
+ port: config.port,
56
+ services: [
57
+ // Custom Services
58
+ sessionService,
59
+ queueService,
60
+ pollingService,
61
+
62
+ // Interval-based services using built-in HeartbeatService
63
+ new HeartbeatService({
64
+ name: 'relay-polling',
65
+ interval: config.pollIntervalMs,
66
+ handler: () => pollingService.poll()
67
+ }),
68
+ new HeartbeatService({
69
+ name: 'queue-processor',
70
+ interval: 1000, // Process queue every second
71
+ handler: () => queueService.processNext()
72
+ }),
73
+ new HeartbeatService({
74
+ name: 'session-cleanup',
75
+ interval: 60000, // Every minute
76
+ handler: () => {
77
+ const now = Date.now();
78
+ const timeout = 240000; // 4 minutes
79
+ for (const [id, lastActivity] of state.sessionActivity) {
80
+ if (now - lastActivity > timeout) {
81
+ state.sessions.delete(id);
82
+ state.sessionActivity.delete(id);
83
+ console.log(`[daemon-v2] Cleaned up stale session ${id}`);
84
+ }
85
+ }
86
+ }
87
+ })
88
+ ]
89
+ });
90
+
91
+ // 6. Start
92
+ try {
93
+ await agent.start();
94
+ console.log('[daemon-v2] Ready');
95
+ } catch (error) {
96
+ console.error('[daemon-v2] Failed to start:', error);
97
+ process.exit(1);
98
+ }
99
+ }
100
+
101
+ main().catch(err => {
102
+ console.error('[daemon-v2] Critical error:', err);
103
+ process.exit(1);
104
+ });