@darksol/terminal 0.10.0 → 0.11.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.
package/README.md CHANGED
@@ -15,7 +15,7 @@ A unified CLI for market intel, trading, AI-powered analysis, on-chain oracle, c
15
15
  [![License: GPL-3.0](https://img.shields.io/badge/License-GPL--3.0-gold.svg)](https://www.gnu.org/licenses/gpl-3.0)
16
16
  [![Node](https://img.shields.io/badge/node-%3E%3D18.0.0-green.svg)](https://nodejs.org/)
17
17
 
18
- - Current release: **0.9.2**
18
+ - Current release: **0.11.0**
19
19
  - Changelog: `CHANGELOG.md`
20
20
 
21
21
  ## Install
@@ -81,6 +81,27 @@ darksol serve
81
81
 
82
82
  # Start agent signer for OpenClaw
83
83
  darksol agent start main
84
+
85
+ # Telegram bot — AI chat through Telegram
86
+ darksol telegram setup
87
+ darksol telegram start
88
+ darksol telegram status
89
+ darksol telegram send 123456789 "Hello from DARKSOL"
90
+
91
+ # Background daemon — manage persistent services
92
+ darksol daemon start
93
+ darksol daemon status
94
+ darksol daemon stop
95
+
96
+ # Browser automation (requires: npm i playwright-core)
97
+ darksol browser launch --headed
98
+ darksol browser navigate https://app.uniswap.org
99
+ darksol browser screenshot swap-page.png
100
+ darksol browser click "#swap-button"
101
+ darksol browser type "#amount-input" "1.0"
102
+ darksol browser eval "document.title"
103
+ darksol browser close
104
+ darksol browser install
84
105
  ```
85
106
 
86
107
  ## `darksol serve` (Web Terminal UX)
@@ -147,11 +168,100 @@ ai <prompt> # chat with trading assistant
147
168
  | `cards` | Crypto → prepaid Visa/MC cards | Service fees |
148
169
  | `builders` | ERC-8021 builder directory + leaderboard | Free |
149
170
  | `facilitator` | x402 payment verification & settlement | Free |
171
+ | `telegram` | Telegram bot — AI chat via Telegram Bot API | Provider dependent |
172
+ | `daemon` | Background service daemon (manages TG, browser, etc.) | Free |
173
+ | `browser` | Playwright-powered browser automation | Free |
150
174
  | `serve` | Local interactive web terminal (xterm.js) | Free |
151
175
  | `config` | Terminal configuration | Free |
152
176
 
153
177
  ---
154
178
 
179
+ ## 📱 Telegram Bot
180
+
181
+ Turn your terminal into a Telegram AI agent. Same brain (LLM + soul + memory), different mouth.
182
+
183
+ ```bash
184
+ # Guided setup — walks you through BotFather
185
+ darksol telegram setup
186
+
187
+ # Start the bot (foreground, or managed by daemon)
188
+ darksol telegram start
189
+
190
+ # Check bot status
191
+ darksol telegram status
192
+
193
+ # Send a direct message
194
+ darksol telegram send <chat_id> "Hello from DARKSOL"
195
+ ```
196
+
197
+ **Setup walkthrough:**
198
+ 1. Open Telegram → search `@BotFather` → send `/newbot`
199
+ 2. Follow BotFather's prompts to name your bot
200
+ 3. Copy the bot token
201
+ 4. Run `darksol telegram setup` → paste token → auto-validates via `getMe`
202
+ 5. Token encrypted and stored in your key vault
203
+ 6. `darksol telegram start` → bot goes live
204
+
205
+ **Features:**
206
+ - Per-chat session memory (remembers conversation context)
207
+ - Soul system prompt (your agent's personality carries over)
208
+ - Built-in commands: `/start`, `/help`, `/status`
209
+ - Typing indicators while LLM processes
210
+ - Rate limiting (1 req/sec per chat)
211
+ - 429 auto-retry for Telegram API limits
212
+ - Daemon-aware: runs foreground solo, or as a managed service
213
+
214
+ ---
215
+
216
+ ## 🖥️ Background Daemon
217
+
218
+ One process to rule them all. Manages persistent services (Telegram bot, browser, future channels).
219
+
220
+ ```bash
221
+ darksol daemon start # Detached background process
222
+ darksol daemon status # PID, uptime, active services
223
+ darksol daemon stop # Graceful shutdown
224
+ darksol daemon restart # Stop + start
225
+ darksol daemon start --port 9999 # Custom health port
226
+ ```
227
+
228
+ **Health endpoint:** `http://localhost:18792/health` — returns uptime, version, active services list.
229
+
230
+ **Service registry:** Services (Telegram, browser, etc.) register with the daemon for managed lifecycle. Start once, everything runs.
231
+
232
+ ---
233
+
234
+ ## 🌐 Browser Automation
235
+
236
+ Playwright-powered browser control — automate dApps, scrape data, take screenshots.
237
+
238
+ ```bash
239
+ # Install browser binary (one-time)
240
+ darksol browser install
241
+
242
+ # Launch and control
243
+ darksol browser launch --headed --type chromium
244
+ darksol browser navigate https://app.uniswap.org
245
+ darksol browser screenshot swap-page.png
246
+ darksol browser click "#connect-wallet"
247
+ darksol browser type "#search" "AERO"
248
+ darksol browser eval "document.title"
249
+ darksol browser status
250
+ darksol browser close
251
+ ```
252
+
253
+ **Requires:** `npm install playwright-core` (optional dependency — only needed if you use browser features).
254
+
255
+ **Features:**
256
+ - Chromium, Firefox, or WebKit
257
+ - Headless (default) or headed mode
258
+ - Named profiles with persistent cookies/sessions (`~/.darksol/browser/profiles/`)
259
+ - IPC via named pipes — CLI commands talk to a running browser instance
260
+ - Web shell integration (`browser` command in `darksol serve`)
261
+ - Daemon-managed when running
262
+
263
+ ---
264
+
155
265
  ## 🔐 Secure Agent Signer
156
266
 
157
267
  **The killer feature.** A PK-isolated signing proxy for AI agents (OpenClaw, etc.).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@darksol/terminal",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "DARKSOL Terminal — unified CLI for all DARKSOL services. Market intel, trading, oracle, casino, and more.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -46,6 +46,9 @@
46
46
  "update-notifier": "^7.3.1",
47
47
  "ws": "^8.19.0"
48
48
  },
49
+ "optionalDependencies": {
50
+ "playwright-core": "^1.52.0"
51
+ },
49
52
  "engines": {
50
53
  "node": ">=18.0.0"
51
54
  }
@@ -0,0 +1,58 @@
1
+ import { sendBrowserCommand } from '../services/browser.js';
2
+
3
+ const DEFAULT_TIMEOUT = 30_000;
4
+
5
+ export async function waitForPage(target, opts = {}) {
6
+ const expression = JSON.stringify(target);
7
+ return sendBrowserCommand('eval', {
8
+ expression: `
9
+ new Promise((resolve) => {
10
+ const target = ${expression};
11
+ const timeout = ${Number(opts.timeout || DEFAULT_TIMEOUT)};
12
+ const start = Date.now();
13
+ const check = () => {
14
+ const matches = typeof target === 'string'
15
+ ? window.location.href.includes(target)
16
+ : true;
17
+ if (matches) return resolve({ ok: true, url: window.location.href });
18
+ if (Date.now() - start > timeout) return resolve({ ok: false, url: window.location.href });
19
+ setTimeout(check, 250);
20
+ };
21
+ check();
22
+ })
23
+ `,
24
+ });
25
+ }
26
+
27
+ export async function fillForm(fields = [], opts = {}) {
28
+ for (const field of fields) {
29
+ await sendBrowserCommand('type', {
30
+ selector: field.selector,
31
+ text: field.value,
32
+ timeout: opts.timeout || DEFAULT_TIMEOUT,
33
+ });
34
+ }
35
+ return true;
36
+ }
37
+
38
+ export async function runLoginFlow(flow = {}) {
39
+ if (flow.url) {
40
+ await sendBrowserCommand('navigate', {
41
+ url: flow.url,
42
+ timeout: flow.timeout || DEFAULT_TIMEOUT,
43
+ });
44
+ }
45
+ if (Array.isArray(flow.fields) && flow.fields.length) {
46
+ await fillForm(flow.fields, flow);
47
+ }
48
+ if (flow.submitSelector) {
49
+ await sendBrowserCommand('click', {
50
+ selector: flow.submitSelector,
51
+ timeout: flow.timeout || DEFAULT_TIMEOUT,
52
+ });
53
+ }
54
+ if (flow.waitFor) {
55
+ await waitForPage(flow.waitFor, flow);
56
+ }
57
+ return sendBrowserCommand('status');
58
+ }
package/src/cli.js CHANGED
@@ -22,6 +22,17 @@ import { cardsCatalog, cardsOrder, cardsStatus } from './services/cards.js';
22
22
  import { facilitatorHealth, facilitatorVerify, facilitatorSettle } from './services/facilitator.js';
23
23
  import { buildersLeaderboard, buildersLookup, buildersFeed } from './services/builders.js';
24
24
  import { createScript, listScripts, runScript, showScript, editScript, deleteScript, cloneScript, listTemplates } from './scripts/engine.js';
25
+ import {
26
+ launchBrowserCommand,
27
+ navigateBrowserCommand,
28
+ browserScreenshotCommand,
29
+ browserClickCommand,
30
+ browserTypeCommand,
31
+ browserEvalCommand,
32
+ browserCloseCommand,
33
+ showBrowserStatus,
34
+ installPlaywrightBrowsers,
35
+ } from './services/browser.js';
25
36
  import { showTradingTips, showScriptTips, showNetworkReference, showQuickStart, showWalletSummary, showTokenInfo, showTxResult } from './utils/helpers.js';
26
37
  import { addKey, removeKey, listKeys } from './config/keys.js';
27
38
  import { parseIntent, startChat, adviseStrategy, analyzeToken, executeIntent } from './llm/intent.js';
@@ -31,6 +42,8 @@ import { runSetupWizard } from './setup/wizard.js';
31
42
  import { displaySoul, hasSoul, resetSoul, runSoulSetup } from './soul/index.js';
32
43
  import { clearMemories, exportMemories, getRecentMemories, searchMemories } from './memory/index.js';
33
44
  import { getAgentStatus, planAgentGoal, runAgentTask } from './agent/index.js';
45
+ import { daemonStart, daemonStop, daemonStatus, daemonRestart } from './daemon/index.js';
46
+ import { telegramSetup, telegramStartForeground, telegramStopCommand, telegramStatusCommand, telegramSendCommand } from './services/telegram.js';
34
47
  import { createRequire } from 'module';
35
48
  import { resolve } from 'path';
36
49
  import { getConfiguredModel, getProviderDefaultModel } from './llm/models.js';
@@ -608,6 +621,64 @@ export function cli(argv) {
608
621
  .option('--no-open', 'Don\'t auto-open browser')
609
622
  .action((opts) => startWebShell(opts));
610
623
 
624
+ const browser = program
625
+ .command('browser')
626
+ .description('Playwright-powered browser automation');
627
+
628
+ browser
629
+ .command('launch')
630
+ .description('Launch a browser instance and keep it running')
631
+ .option('--headed', 'Launch with a visible browser window')
632
+ .option('--type <browser>', 'Browser type', 'chromium')
633
+ .option('--profile <name>', 'Browser profile name', 'default')
634
+ .action((opts) => launchBrowserCommand(opts));
635
+
636
+ browser
637
+ .command('navigate <url>')
638
+ .description('Navigate the active page to a URL')
639
+ .action((url) => navigateBrowserCommand(url));
640
+
641
+ browser
642
+ .command('screenshot [filename]')
643
+ .description('Capture a screenshot of the active page')
644
+ .action((filename) => browserScreenshotCommand(filename));
645
+
646
+ browser
647
+ .command('click <selector>')
648
+ .description('Click an element on the active page')
649
+ .action((selector) => browserClickCommand(selector));
650
+
651
+ browser
652
+ .command('type <selector> <text>')
653
+ .description('Type text into an element on the active page')
654
+ .action((selector, text) => browserTypeCommand(selector, text));
655
+
656
+ browser
657
+ .command('eval <js>')
658
+ .description('Evaluate JavaScript in the active page')
659
+ .action((js) => browserEvalCommand(js));
660
+
661
+ browser
662
+ .command('close')
663
+ .description('Close the running browser service')
664
+ .action(() => browserCloseCommand());
665
+
666
+ browser
667
+ .command('status')
668
+ .description('Show current browser state')
669
+ .action(() => showBrowserStatus());
670
+
671
+ browser
672
+ .command('install')
673
+ .description('Install a Playwright browser binary after user confirmation')
674
+ .action(async () => {
675
+ try {
676
+ await installPlaywrightBrowsers();
677
+ } catch (err) {
678
+ error(err.message);
679
+ }
680
+ });
681
+
611
682
  // ═══════════════════════════════════════
612
683
  // PORTFOLIO SHORTCUT
613
684
  // ═══════════════════════════════════════
@@ -1031,6 +1102,68 @@ export function cli(argv) {
1031
1102
  .description('Uninstall a skill')
1032
1103
  .action((name) => uninstallSkill(name));
1033
1104
 
1105
+ // ═══════════════════════════════════════
1106
+ // DAEMON COMMANDS
1107
+ // ═══════════════════════════════════════
1108
+ const daemon = program
1109
+ .command('daemon')
1110
+ .description('Background daemon - manage persistent services');
1111
+
1112
+ daemon
1113
+ .command('start')
1114
+ .description('Start the background daemon')
1115
+ .option('-p, --port <port>', 'Health server port', '18792')
1116
+ .action((opts) => daemonStart(opts));
1117
+
1118
+ daemon
1119
+ .command('stop')
1120
+ .description('Stop the background daemon')
1121
+ .action(() => daemonStop());
1122
+
1123
+ daemon
1124
+ .command('status')
1125
+ .description('Show daemon status and health')
1126
+ .option('-p, --port <port>', 'Health server port', '18792')
1127
+ .action((opts) => daemonStatus(opts));
1128
+
1129
+ daemon
1130
+ .command('restart')
1131
+ .description('Restart the daemon')
1132
+ .option('-p, --port <port>', 'Health server port', '18792')
1133
+ .action((opts) => daemonRestart(opts));
1134
+
1135
+ // ═══════════════════════════════════════
1136
+ // TELEGRAM COMMANDS
1137
+ // ═══════════════════════════════════════
1138
+ const telegram = program
1139
+ .command('telegram')
1140
+ .description('Telegram bot - AI chat via Telegram');
1141
+
1142
+ telegram
1143
+ .command('setup')
1144
+ .description('Interactive Telegram bot setup with BotFather')
1145
+ .action(() => telegramSetup());
1146
+
1147
+ telegram
1148
+ .command('start')
1149
+ .description('Start the Telegram bot (foreground)')
1150
+ .action(() => telegramStartForeground());
1151
+
1152
+ telegram
1153
+ .command('stop')
1154
+ .description('Stop the Telegram bot')
1155
+ .action(() => telegramStopCommand());
1156
+
1157
+ telegram
1158
+ .command('status')
1159
+ .description('Show bot info and connection state')
1160
+ .action(() => telegramStatusCommand());
1161
+
1162
+ telegram
1163
+ .command('send <chatId> <message...>')
1164
+ .description('Send a direct message to a chat')
1165
+ .action((chatId, message) => telegramSendCommand(chatId, message));
1166
+
1034
1167
  // ═══════════════════════════════════════
1035
1168
  // TIPS & REFERENCE COMMANDS
1036
1169
  // ═══════════════════════════════════════
@@ -1602,6 +1735,9 @@ function showCommandList() {
1602
1735
  ['mail', 'AgentMail - email for your agent'],
1603
1736
  ['facilitator', 'x402 payment facilitator'],
1604
1737
  ['skills', 'Agent skill directory'],
1738
+ ['browser', 'Playwright browser automation'],
1739
+ ['daemon', 'Background service daemon'],
1740
+ ['telegram', 'Telegram bot - AI chat'],
1605
1741
  ['serve', 'Launch web terminal in browser'],
1606
1742
  ['setup', 'Re-run setup wizard'],
1607
1743
  ['config', 'Terminal configuration'],
@@ -178,6 +178,16 @@ export const SERVICES = {
178
178
  docsUrl: 'https://console.agentmail.to',
179
179
  validate: (key) => key.startsWith('am_'),
180
180
  },
181
+
182
+ // Messaging
183
+ telegram: {
184
+ name: 'Telegram Bot',
185
+ category: 'messaging',
186
+ description: 'Telegram bot token — AI chat via Telegram',
187
+ envVar: 'TELEGRAM_BOT_TOKEN',
188
+ docsUrl: 'https://core.telegram.org/bots#botfather',
189
+ validate: (key) => /^\d+:.+$/.test(key),
190
+ },
181
191
  paraswap: {
182
192
  name: 'ParaSwap',
183
193
  category: 'trading',
@@ -319,8 +329,8 @@ export function listKeys() {
319
329
 
320
330
  showSection('API KEY VAULT');
321
331
 
322
- const categories = ['llm', 'data', 'rpc', 'trading', 'email'];
323
- const catNames = { llm: '🧠 LLM PROVIDERS', data: '📊 DATA PROVIDERS', rpc: '🌐 RPC PROVIDERS', trading: '📈 TRADING', email: '📧 EMAIL' };
332
+ const categories = ['llm', 'data', 'rpc', 'trading', 'email', 'messaging'];
333
+ const catNames = { llm: '🧠 LLM PROVIDERS', data: '📊 DATA PROVIDERS', rpc: '🌐 RPC PROVIDERS', trading: '📈 TRADING', email: '📧 EMAIL', messaging: '💬 MESSAGING' };
324
334
 
325
335
  for (const cat of categories) {
326
336
  console.log('');
@@ -0,0 +1,225 @@
1
+ import { spawn, execSync } from 'child_process';
2
+ import { createServer } from 'http';
3
+ import { appendFileSync, mkdirSync, existsSync } from 'fs';
4
+ import { join } from 'path';
5
+ import { homedir } from 'os';
6
+ import { fileURLToPath } from 'url';
7
+ import { writePid, readPid, removePid, getDaemonStatus, DARKSOL_DIR } from './pid.js';
8
+ import { getAllServiceStatus, stopAllServices, listServices } from './manager.js';
9
+ import { theme } from '../ui/theme.js';
10
+ import { spinner, kvDisplay, success, error, warn, info } from '../ui/components.js';
11
+ import { showSection } from '../ui/banner.js';
12
+ import { createRequire } from 'module';
13
+ import fetch from 'node-fetch';
14
+
15
+ const require = createRequire(import.meta.url);
16
+ const { version: PKG_VERSION } = require('../../package.json');
17
+
18
+ const LOGS_DIR = join(DARKSOL_DIR, 'logs');
19
+ const LOG_FILE = join(LOGS_DIR, 'daemon.log');
20
+ const DEFAULT_PORT = 18792;
21
+
22
+ function ensureLogsDir() {
23
+ if (!existsSync(LOGS_DIR)) mkdirSync(LOGS_DIR, { recursive: true });
24
+ }
25
+
26
+ function daemonLog(msg) {
27
+ ensureLogsDir();
28
+ const ts = new Date().toISOString();
29
+ appendFileSync(LOG_FILE, `[${ts}] ${msg}\n`, 'utf8');
30
+ }
31
+
32
+ /**
33
+ * Start the daemon as a detached background process.
34
+ * @param {object} [opts]
35
+ * @param {number} [opts.port]
36
+ */
37
+ export async function daemonStart(opts = {}) {
38
+ const status = getDaemonStatus();
39
+ if (status.running) {
40
+ warn(`Daemon already running (PID ${status.pid})`);
41
+ return;
42
+ }
43
+
44
+ const port = parseInt(opts.port, 10) || DEFAULT_PORT;
45
+ const entryScript = fileURLToPath(new URL('./index.js', import.meta.url));
46
+
47
+ const child = spawn(process.execPath, [entryScript, '--daemon-run', String(port)], {
48
+ detached: true,
49
+ stdio: 'ignore',
50
+ env: { ...process.env, DARKSOL_DAEMON: '1' },
51
+ });
52
+
53
+ child.unref();
54
+
55
+ if (child.pid) {
56
+ writePid(child.pid);
57
+ success(`Daemon started (PID ${child.pid}, port ${port})`);
58
+ info(`Logs: ${LOG_FILE}`);
59
+ info(`Health: http://localhost:${port}/health`);
60
+ } else {
61
+ error('Failed to start daemon process');
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Stop the running daemon.
67
+ */
68
+ export async function daemonStop() {
69
+ const status = getDaemonStatus();
70
+ if (!status.running) {
71
+ warn('Daemon is not running');
72
+ return;
73
+ }
74
+
75
+ const spin = spinner('Stopping daemon...').start();
76
+
77
+ try {
78
+ if (process.platform === 'win32') {
79
+ execSync(`taskkill /PID ${status.pid} /F /T`, { stdio: 'ignore' });
80
+ } else {
81
+ process.kill(status.pid, 'SIGTERM');
82
+ }
83
+ removePid();
84
+ spin.succeed(`Daemon stopped (PID ${status.pid})`);
85
+ } catch (err) {
86
+ removePid();
87
+ spin.fail('Daemon stop had issues');
88
+ warn(err.message);
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Show daemon status — process check + health endpoint query.
94
+ * @param {object} [opts]
95
+ * @param {number} [opts.port]
96
+ */
97
+ export async function daemonStatus(opts = {}) {
98
+ const port = parseInt(opts.port, 10) || DEFAULT_PORT;
99
+ const status = getDaemonStatus();
100
+
101
+ showSection('DAEMON STATUS');
102
+
103
+ if (!status.running) {
104
+ kvDisplay([
105
+ ['Process', theme.dim('not running')],
106
+ ['PID File', theme.dim('none')],
107
+ ]);
108
+ console.log('');
109
+ info('Start with: darksol daemon start');
110
+ console.log('');
111
+ return;
112
+ }
113
+
114
+ // Process is alive — try health endpoint
115
+ let health = null;
116
+ try {
117
+ const res = await fetch(`http://localhost:${port}/health`, { timeout: 3000 });
118
+ if (res.ok) health = await res.json();
119
+ } catch {
120
+ // health endpoint unreachable
121
+ }
122
+
123
+ const pairs = [
124
+ ['Process', theme.success(`running (PID ${status.pid})`)],
125
+ ['Port', String(port)],
126
+ ];
127
+
128
+ if (health) {
129
+ pairs.push(['Uptime', `${Math.round(health.uptime)}s`]);
130
+ pairs.push(['Version', health.version || PKG_VERSION]);
131
+ pairs.push(['Services', health.services?.length ? health.services.join(', ') : theme.dim('none')]);
132
+ } else {
133
+ pairs.push(['Health', theme.warning('unreachable')]);
134
+ }
135
+
136
+ pairs.push(['Log', LOG_FILE]);
137
+ kvDisplay(pairs);
138
+ console.log('');
139
+ }
140
+
141
+ /**
142
+ * Restart the daemon (stop + start).
143
+ * @param {object} [opts]
144
+ */
145
+ export async function daemonRestart(opts = {}) {
146
+ const status = getDaemonStatus();
147
+ if (status.running) {
148
+ await daemonStop();
149
+ // Brief pause to let the OS release the port
150
+ await new Promise((r) => setTimeout(r, 500));
151
+ }
152
+ await daemonStart(opts);
153
+ }
154
+
155
+ // ─────────────────────────────────────
156
+ // DAEMON PROCESS ENTRY (run by child)
157
+ // ─────────────────────────────────────
158
+
159
+ /**
160
+ * Run the actual daemon process (HTTP health server + service management).
161
+ * Called when the script is executed directly with --daemon-run.
162
+ * @param {number} port
163
+ */
164
+ export async function runDaemonProcess(port) {
165
+ const startTime = Date.now();
166
+
167
+ daemonLog(`Daemon starting on port ${port} (PID ${process.pid})`);
168
+
169
+ const server = createServer((req, res) => {
170
+ if (req.url === '/health' && req.method === 'GET') {
171
+ const uptimeSec = (Date.now() - startTime) / 1000;
172
+ const services = getAllServiceStatus();
173
+ const body = JSON.stringify({
174
+ status: 'ok',
175
+ pid: process.pid,
176
+ uptime: uptimeSec,
177
+ version: PKG_VERSION,
178
+ services: services.map((s) => s.name),
179
+ serviceDetails: services,
180
+ });
181
+ res.writeHead(200, { 'Content-Type': 'application/json' });
182
+ res.end(body);
183
+ return;
184
+ }
185
+
186
+ res.writeHead(404, { 'Content-Type': 'application/json' });
187
+ res.end(JSON.stringify({ error: 'not found' }));
188
+ });
189
+
190
+ server.listen(port, '127.0.0.1', () => {
191
+ daemonLog(`Health server listening on http://127.0.0.1:${port}/health`);
192
+ });
193
+
194
+ // Graceful shutdown
195
+ const shutdown = async () => {
196
+ daemonLog('Shutting down daemon...');
197
+ await stopAllServices();
198
+ server.close();
199
+ removePid();
200
+ daemonLog('Daemon stopped');
201
+ process.exit(0);
202
+ };
203
+
204
+ process.on('SIGTERM', shutdown);
205
+ process.on('SIGINT', shutdown);
206
+ process.on('uncaughtException', (err) => {
207
+ daemonLog(`Uncaught exception: ${err.message}`);
208
+ });
209
+ process.on('unhandledRejection', (err) => {
210
+ daemonLog(`Unhandled rejection: ${err}`);
211
+ });
212
+ }
213
+
214
+ // ─────────────────────────────────────
215
+ // SELF-EXECUTION: when run directly as daemon child process
216
+ // ─────────────────────────────────────
217
+
218
+ const args = process.argv.slice(2);
219
+ if (args[0] === '--daemon-run') {
220
+ const port = parseInt(args[1], 10) || DEFAULT_PORT;
221
+ writePid(process.pid);
222
+ runDaemonProcess(port);
223
+ }
224
+
225
+ export { DEFAULT_PORT, LOG_FILE };