@agent-webui/ai-desk-daemon 1.0.17

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 ADDED
@@ -0,0 +1,413 @@
1
+ # AI Desk Desktop
2
+
3
+ 一个类似 Docker Desktop 的桌面应用,用于管理本地 AI CLI 工具的守护进程。
4
+
5
+ <p align="center">
6
+ <img src="docs/images/screenshot.png" alt="AI Desk Desktop" width="800">
7
+ </p>
8
+
9
+ ## ✨ 特性
10
+
11
+ - 🎯 **自动启动** - Web 访问时自动检测并启动守护进程
12
+ - 🖥️ **图形化管理** - 系统托盘 + 控制面板,便捷管理
13
+ - 🔄 **实时监控** - 查看守护进程状态、日志和统计信息
14
+ - 🚀 **智能启动器** - 多种方式自动唤醒守护进程
15
+ - 🔒 **安全可靠** - 工作目录白名单、认证支持
16
+ - 📦 **跨平台** - macOS、Windows、Linux 统一体验
17
+ - ⚡ **高性能** - 基于 Tauri,体积小(~15MB)、内存占用低
18
+
19
+ ## 📦 安装
20
+
21
+ ### 方式 1: npm 安装(纯 CLI 工具)
22
+
23
+ **适合场景**:开发者、服务器环境、CI/CD、命令行用户
24
+
25
+ ```bash
26
+ # 全局安装
27
+ npm install -g @eden_qu/ai-desk-daemon
28
+
29
+ # 启动 daemon
30
+ aidesk start
31
+
32
+ # 查看状态
33
+ aidesk status
34
+ ```
35
+
36
+ **包含内容**:
37
+ - ✅ AI Desk Daemon 后台服务
38
+ - ✅ CLI 命令行管理工具
39
+ - ✅ HTTP API (http://localhost:9527)
40
+
41
+ **不包含**:
42
+ - ❌ 系统托盘应用 (Tray)
43
+ - ❌ 桌面 GUI 应用
44
+
45
+ **可用命令**:
46
+ - `aidesk start` - 启动守护进程(后台运行)
47
+ - `aidesk start --log` - 启动守护进程(前台运行,跟随日志)
48
+ - `aidesk stop` - 停止守护进程
49
+ - `aidesk restart` - 重启守护进程
50
+ - `aidesk status` - 查看状态
51
+ - `aidesk logs` - 查看日志
52
+ - `aidesk logs -f` - 实时查看日志(不会停止守护进程)
53
+
54
+ 📖 详细使用说明:[NPM_CLI.md](NPM_CLI.md)
55
+
56
+ ---
57
+
58
+ ### 方式 2: 完整安装(Daemon + Tray)
59
+
60
+ **适合场景**:桌面用户、需要系统托盘图标、完整 GUI 体验
61
+
62
+ #### macOS
63
+
64
+ ```bash
65
+ # 下载并安装
66
+ curl -fsSL https://github.com/your-repo/ai-desk-desktop/releases/latest/download/install-macos.sh | bash
67
+
68
+ # 或手动安装
69
+ ./scripts/install-macos.sh
70
+ ```
71
+
72
+ #### Linux
73
+
74
+ ```bash
75
+ # Ubuntu/Debian
76
+ sudo ./scripts/install-linux.sh
77
+
78
+ # 启用自动启动
79
+ systemctl --user enable ai-desk-daemon
80
+ systemctl --user start ai-desk-daemon
81
+ ```
82
+
83
+ #### Windows
84
+
85
+ ```powershell
86
+ # 以管理员身份运行 PowerShell
87
+ .\scripts\install-windows.ps1
88
+ ```
89
+
90
+ ## 🚀 快速开始
91
+
92
+ ### 1. 启动桌面应用
93
+
94
+ **macOS/Linux:**
95
+ ```bash
96
+ # 从应用程序菜单启动
97
+ open /Applications/AI\ Desk\ Desktop.app
98
+
99
+ # 或命令行启动
100
+ ai-desk-desktop
101
+ ```
102
+
103
+ **Windows:**
104
+ - 从开始菜单启动 "AI Desk Desktop"
105
+
106
+ ### 2. Web 应用集成
107
+
108
+ 在你的 Web 应用中集成智能启动器:
109
+
110
+ ```tsx
111
+ // App.tsx
112
+ import DaemonInitializer from './components/common/DaemonInitializer';
113
+
114
+ function App() {
115
+ return (
116
+ <DaemonInitializer>
117
+ <YourMainApp />
118
+ </DaemonInitializer>
119
+ );
120
+ }
121
+ ```
122
+
123
+ 启动器会自动:
124
+ - ✅ 检测守护进程是否运行
125
+ - ✅ 如果未运行,通过多种方式尝试启动
126
+ - ✅ 等待守护进程就绪
127
+ - ✅ 失败时显示友好的错误提示
128
+
129
+ ### 3. 使用 Daemon API
130
+
131
+ ```typescript
132
+ import { daemonService } from './services/DaemonService';
133
+
134
+ // 执行 CLI 命令
135
+ const result = await daemonService.executeCLI({
136
+ command: 'claude',
137
+ args: ['--verbose'],
138
+ stdin: 'Your prompt here',
139
+ cwd: '/path/to/workspace',
140
+ timeout: 300000,
141
+ });
142
+
143
+ // 流式执行
144
+ await daemonService.executeStreaming(
145
+ { command: 'claude', args: [], stdin: 'Hello!' },
146
+ (chunk) => console.log(chunk), // onChunk
147
+ (result) => console.log(result), // onComplete
148
+ (error) => console.error(error) // onError
149
+ );
150
+ ```
151
+
152
+ ## 🏗️ 架构
153
+
154
+ ```
155
+ ┌─────────────────────────────────────────────────────┐
156
+ │ AI Desk Desktop App │
157
+ │ (Tauri Application) │
158
+ │ │
159
+ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
160
+ │ │ System │ │ Control │ │ Settings │ │
161
+ │ │ Tray │ │ Panel │ │ Panel │ │
162
+ │ └──────────┘ └──────────┘ └──────────┘ │
163
+ │ │ │ │
164
+ │ └────────────────┼──────────────────────────┘
165
+ │ ▼
166
+ │ ┌──────────────────┐
167
+ │ │ Daemon Process │
168
+ │ │ HTTP Server :9527│
169
+ │ └──────────────────┘
170
+ │ │
171
+ │ ┌────────────────┼────────────────┐
172
+ │ ▼ ▼ ▼
173
+ │ ┌────────┐ ┌────────┐ ┌────────┐
174
+ │ │ Claude │ │ Gemini │ │ Cursor │
175
+ │ └────────┘ └────────┘ └────────┘
176
+
177
+ ├─────────────────────────────────────────────────────┤
178
+ │ Web Application │
179
+ │ (React/TypeScript) │
180
+ │ │
181
+ │ ┌────────────────────────────────────┐ │
182
+ │ │ Smart Daemon Starter │ │
183
+ │ │ - Auto-detect & Start │ │
184
+ │ │ - URL Scheme / Extension / HTTP │ │
185
+ │ └────────────────────────────────────┘ │
186
+ └─────────────────────────────────────────────────────┘
187
+ ```
188
+
189
+ ## 📚 组件说明
190
+
191
+ ### 1. Daemon(守护进程)
192
+
193
+ 基于 Go 开发的 HTTP/WebSocket 服务器:
194
+
195
+ **功能**:
196
+ - CLI 检测(claude、gemini、cursor)
197
+ - CLI 执行(同步和流式)
198
+ - 并发控制和超时管理
199
+ - 安全策略(工作目录白名单)
200
+ - 日志记录
201
+
202
+ **API 端点**:
203
+ - `GET /health` - 健康检查
204
+ - `GET /api/v1/clis` - 检测 CLI
205
+ - `POST /api/v1/execute` - 执行 CLI(REST)
206
+ - `WS /api/v1/execute/stream` - 执行 CLI(WebSocket)
207
+ - `POST /api/v1/execute/{id}/cancel` - 取消执行
208
+
209
+ ### 2. Tauri Desktop(桌面应用)
210
+
211
+ 基于 Tauri(Rust + React)的桌面应用:
212
+
213
+ **功能**:
214
+ - 系统托盘图标和菜单
215
+ - 守护进程生命周期管理
216
+ - 实时状态监控
217
+ - 日志查看
218
+ - 设置管理
219
+ - URL Scheme 注册
220
+
221
+ ### 3. Web Integration(Web 集成)
222
+
223
+ **Smart Daemon Starter(智能启动器)**:
224
+ - 自动检测守护进程
225
+ - 多种启动方式(URL Scheme、Extension、HTTP)
226
+ - 自动重试机制
227
+ - 友好的错误提示
228
+
229
+ **Daemon Service(守护进程服务)**:
230
+ - HTTP/WebSocket 通信
231
+ - CLI 执行和流式输出
232
+ - 错误处理
233
+
234
+ ## ⚙️ 配置
235
+
236
+ ### Daemon 配置
237
+
238
+ 配置文件位置:`~/.aidesktop/daemon-config.json`
239
+
240
+ ```json
241
+ {
242
+ "port": 9527,
243
+ "max_concurrent_executions": 10,
244
+ "execution_timeout": 300,
245
+ "allowed_origins": [
246
+ "*"
247
+ ],
248
+ "allowed_working_dirs": [
249
+ "/path/to/your/home"
250
+ ],
251
+ "require_authentication": false,
252
+ "auth_token": "",
253
+ "log_level": "INFO"
254
+ }
255
+ ```
256
+
257
+ 说明:`9527` 是默认端口,实际运行时以 `~/.aidesktop/daemon-config.json` 中的 `port` 为准。
258
+
259
+ ### 应用配置
260
+
261
+ 配置文件位置:`~/.aidesktop/app-config.json`
262
+
263
+ ```json
264
+ {
265
+ "auto_start": true,
266
+ "minimize_to_tray": true,
267
+ "daemon_port": 9527
268
+ }
269
+ ```
270
+
271
+ 如果你修改了 daemon 配置端口,请保持这里的 `daemon_port` 与之同步。
272
+
273
+ ## 🔧 开发
274
+
275
+ ### 前置要求
276
+
277
+ - **Node.js** 18+
278
+ - **Rust** 1.70+
279
+ - **Go** 1.21+ (用于守护进程)
280
+ - **Tauri CLI** 1.5+
281
+
282
+ ### 构建
283
+
284
+ ```bash
285
+ # 克隆仓库
286
+ git clone https://github.com/your-repo/ai-desk-desktop.git
287
+ cd ai-desk-desktop
288
+
289
+ # 安装依赖
290
+ npm install
291
+
292
+ # 构建所有组件
293
+ ./scripts/build.sh
294
+
295
+ # 或分别构建:
296
+
297
+ # 1. 构建守护进程
298
+ cd daemon && go build -o ai-desk-daemon
299
+
300
+ # 2. 构建前端
301
+ npm run build
302
+
303
+ # 3. 构建 Tauri 应用
304
+ npm run tauri build
305
+ ```
306
+
307
+ ### 开发模式
308
+
309
+ ```bash
310
+ # 终端 1: 启动守护进程
311
+ cd daemon
312
+ go run .
313
+
314
+ # 终端 2: 启动 Tauri 应用
315
+ npm run tauri:dev
316
+ ```
317
+
318
+ ## 📖 使用示例
319
+
320
+ ### 在 Web 应用中显示守护进程状态
321
+
322
+ ```tsx
323
+ import { DaemonStatusIndicator } from './components/common/DaemonStatusBanner';
324
+
325
+ function Layout() {
326
+ return (
327
+ <div>
328
+ <Header />
329
+ <Content />
330
+ <DaemonStatusIndicator /> {/* 显示在右下角 */}
331
+ </div>
332
+ );
333
+ }
334
+ ```
335
+
336
+ ### 条件渲染(守护进程可用时)
337
+
338
+ ```tsx
339
+ import { useDaemon } from './hooks/useDaemon';
340
+
341
+ function AgentPanel() {
342
+ const { isAvailable, isChecking } = useDaemon();
343
+
344
+ if (isChecking) {
345
+ return <div>Checking daemon...</div>;
346
+ }
347
+
348
+ if (!isAvailable) {
349
+ return (
350
+ <div>
351
+ Daemon not available.
352
+ <button onClick={() => window.location.href = 'aidesktop://start'}>
353
+ Launch Now
354
+ </button>
355
+ </div>
356
+ );
357
+ }
358
+
359
+ return <YourAgentInterface />;
360
+ }
361
+ ```
362
+
363
+ ## 🐛 故障排查
364
+
365
+ ### macOS Gatekeeper 警告
366
+
367
+ **问题**:macOS 阻止运行未签名的应用
368
+
369
+ **解决方案**:
370
+ ```bash
371
+ # 移除隔离属性
372
+ xattr -cr "/Applications/AI Desk Desktop.app"
373
+ ```
374
+
375
+ ### 守护进程未启动
376
+
377
+ **检查日志**:
378
+ ```bash
379
+ tail -f ~/.aidesktop/logs/daemon.log
380
+ ```
381
+
382
+ **手动启动测试**:
383
+ ```bash
384
+ /usr/local/bin/ai-desk-daemon --config ~/.aidesktop/daemon-config.json
385
+ ```
386
+
387
+ ### Web 应用无法连接
388
+
389
+ 1. 检查守护进程是否运行:
390
+ ```bash
391
+ curl http://localhost:<port>/health
392
+ ```
393
+
394
+ 2. 检查防火墙设置
395
+ 3. 确认配置文件中的端口未被占用
396
+
397
+ ## 📄 许可证
398
+
399
+ MIT License
400
+
401
+ ## 🤝 贡献
402
+
403
+ 欢迎提交 Issues 和 Pull Requests!
404
+
405
+ ## 📧 支持
406
+
407
+ - GitHub Issues: https://github.com/your-repo/ai-desk-desktop/issues
408
+ - 文档: https://docs.aidesktop.com
409
+
410
+ ---
411
+
412
+ **Version**: 1.0.0
413
+ **Maintained by**: AI Desk Team
package/bin/cli.js ADDED
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * AI Desk Daemon CLI
5
+ */
6
+
7
+ const { program } = require('commander');
8
+ const chalk = require('chalk');
9
+ const fs = require('fs');
10
+ const { start, stop, restart, status } = require('../lib/daemon-manager');
11
+ const { getLogPath } = require('../lib/platform');
12
+ const { VERSION } = require('../lib/platform');
13
+
14
+ program
15
+ .name('ai-desk-daemon')
16
+ .description('AI Desk Daemon - CLI tool for managing the AI Desk daemon service')
17
+ .version(VERSION);
18
+
19
+ // Start command
20
+ program
21
+ .command('start')
22
+ .description('Start the daemon')
23
+ .option('--log', 'Follow daemon logs in foreground (Ctrl+C to stop daemon)')
24
+ .action(async (options) => {
25
+ try {
26
+ const { getPidPath } = require('../lib/platform');
27
+ const daemonPid = start();
28
+ console.log(chalk.green('✓ Daemon started successfully'));
29
+
30
+ // Only follow logs if --log is specified
31
+ if (options.log) {
32
+ console.log(chalk.cyan('\n📋 Following daemon logs (Ctrl+C to stop daemon)...\n'));
33
+
34
+ // Wait a moment for daemon to start writing logs
35
+ await new Promise(resolve => setTimeout(resolve, 1000));
36
+
37
+ const logPath = getLogPath();
38
+ if (!fs.existsSync(logPath)) {
39
+ console.log(chalk.yellow('Waiting for logs...'));
40
+ await new Promise(resolve => setTimeout(resolve, 2000));
41
+ }
42
+
43
+ // Follow logs (tail -f) - works on Unix-like systems
44
+ if (process.platform !== 'win32') {
45
+ const { spawn } = require('child_process');
46
+ const tail = spawn('tail', ['-f', logPath]);
47
+
48
+ tail.stdout.on('data', (data) => {
49
+ process.stdout.write(data);
50
+ });
51
+
52
+ tail.stderr.on('data', (data) => {
53
+ process.stderr.write(data);
54
+ });
55
+
56
+ tail.on('error', (error) => {
57
+ console.error(chalk.red('Failed to follow logs:'), error.message);
58
+ process.exit(1);
59
+ });
60
+
61
+ // Handle Ctrl+C - stop the daemon
62
+ process.on('SIGINT', () => {
63
+ console.log(chalk.yellow('\n\n⏹ Stopping daemon...'));
64
+ tail.kill();
65
+
66
+ try {
67
+ // Stop the daemon
68
+ const { stop } = require('../lib/daemon-manager');
69
+ stop();
70
+ console.log(chalk.green('✓ Daemon stopped successfully'));
71
+ } catch (error) {
72
+ console.error(chalk.red('✗ Failed to stop daemon:'), error.message);
73
+ }
74
+
75
+ process.exit(0);
76
+ });
77
+ } else {
78
+ // Windows: use polling to read log file
79
+ console.log(chalk.yellow('Log following on Windows - press Ctrl+C to stop daemon\n'));
80
+
81
+ let lastSize = 0;
82
+ const pollInterval = setInterval(() => {
83
+ try {
84
+ const stats = fs.statSync(logPath);
85
+ if (stats.size > lastSize) {
86
+ const stream = fs.createReadStream(logPath, {
87
+ start: lastSize,
88
+ end: stats.size
89
+ });
90
+ stream.on('data', (chunk) => {
91
+ process.stdout.write(chunk);
92
+ });
93
+ lastSize = stats.size;
94
+ }
95
+ } catch (error) {
96
+ // Ignore errors
97
+ }
98
+ }, 500);
99
+
100
+ process.on('SIGINT', () => {
101
+ clearInterval(pollInterval);
102
+ console.log(chalk.yellow('\n\n⏹ Stopping daemon...'));
103
+
104
+ try {
105
+ // Stop the daemon
106
+ const { stop } = require('../lib/daemon-manager');
107
+ stop();
108
+ console.log(chalk.green('✓ Daemon stopped successfully'));
109
+ } catch (error) {
110
+ console.error(chalk.red('✗ Failed to stop daemon:'), error.message);
111
+ }
112
+
113
+ process.exit(0);
114
+ });
115
+ }
116
+ }
117
+ } catch (error) {
118
+ console.error(chalk.red('✗ Failed to start daemon:'), error.message);
119
+ process.exit(1);
120
+ }
121
+ });
122
+
123
+ // Stop command
124
+ program
125
+ .command('stop')
126
+ .description('Stop the daemon')
127
+ .action(async () => {
128
+ try {
129
+ stop();
130
+ console.log(chalk.green('✓ Daemon stopped successfully'));
131
+ } catch (error) {
132
+ console.error(chalk.red('✗ Failed to stop daemon:'), error.message);
133
+ process.exit(1);
134
+ }
135
+ });
136
+
137
+ // Restart command
138
+ program
139
+ .command('restart')
140
+ .description('Restart the daemon')
141
+ .action(async () => {
142
+ try {
143
+ restart();
144
+ console.log(chalk.green('✓ Daemon restarted successfully'));
145
+ } catch (error) {
146
+ console.error(chalk.red('✗ Failed to restart daemon:'), error.message);
147
+ process.exit(1);
148
+ }
149
+ });
150
+
151
+ // Status command
152
+ program
153
+ .command('status')
154
+ .description('Check daemon status')
155
+ .action(async () => {
156
+ try {
157
+ const statusInfo = await status();
158
+
159
+ console.log('\n' + chalk.bold('AI Desk Daemon Status'));
160
+ console.log('─'.repeat(40));
161
+
162
+ if (statusInfo.running) {
163
+ console.log(chalk.green('● Running'));
164
+
165
+ if (statusInfo.health && statusInfo.health.data) {
166
+ const { status: healthStatus, version } = statusInfo.health.data;
167
+ console.log(`Version: ${version || 'unknown'}`);
168
+ console.log(`Status: ${healthStatus}`);
169
+ }
170
+
171
+ console.log(`Port: ${statusInfo.port}`);
172
+ console.log(`Dashboard: http://localhost:${statusInfo.port}/daemon-dashboard.html`);
173
+ } else {
174
+ console.log(chalk.red('○ Stopped'));
175
+ }
176
+
177
+ console.log(`Logs: ${statusInfo.logPath}`);
178
+ console.log('');
179
+ } catch (error) {
180
+ console.error(chalk.red('✗ Failed to get status:'), error.message);
181
+ process.exit(1);
182
+ }
183
+ });
184
+
185
+ // Logs command
186
+ program
187
+ .command('logs')
188
+ .description('View daemon logs')
189
+ .option('-f, --follow', 'Follow log output')
190
+ .option('-n, --lines <number>', 'Number of lines to show', '50')
191
+ .action((options) => {
192
+ const logPath = getLogPath();
193
+
194
+ if (!fs.existsSync(logPath)) {
195
+ console.log(chalk.yellow('No logs found'));
196
+ return;
197
+ }
198
+
199
+ if (options.follow) {
200
+ // Follow logs (tail -f)
201
+ const { spawn } = require('child_process');
202
+ const tail = spawn('tail', ['-f', logPath]);
203
+
204
+ tail.stdout.on('data', (data) => {
205
+ process.stdout.write(data);
206
+ });
207
+
208
+ tail.on('error', (error) => {
209
+ console.error(chalk.red('Failed to follow logs:'), error.message);
210
+ process.exit(1);
211
+ });
212
+ } else {
213
+ // Show last N lines
214
+ const { execSync } = require('child_process');
215
+ try {
216
+ const output = execSync(`tail -n ${options.lines} "${logPath}"`, { encoding: 'utf8' });
217
+ console.log(output);
218
+ } catch (error) {
219
+ console.error(chalk.red('Failed to read logs:'), error.message);
220
+ process.exit(1);
221
+ }
222
+ }
223
+ });
224
+
225
+ program.parse(process.argv);
226
+
227
+ // Show help if no command provided
228
+ if (!process.argv.slice(2).length) {
229
+ program.outputHelp();
230
+ }
231
+
package/lib/config.js ADDED
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Configuration management
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const { getConfigDir, getConfigPath } = require('./platform');
8
+
9
+ const DEFAULT_PORT = 9527;
10
+
11
+ /**
12
+ * Ensure config directory exists
13
+ */
14
+ function ensureConfigDir() {
15
+ const configDir = getConfigDir();
16
+ const logsDir = path.join(configDir, 'logs');
17
+
18
+ if (!fs.existsSync(configDir)) {
19
+ fs.mkdirSync(configDir, { recursive: true });
20
+ }
21
+
22
+ if (!fs.existsSync(logsDir)) {
23
+ fs.mkdirSync(logsDir, { recursive: true });
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Load config from file
29
+ */
30
+ function loadConfig() {
31
+ ensureConfigDir();
32
+
33
+ const configPath = getConfigPath();
34
+
35
+ if (!fs.existsSync(configPath)) {
36
+ // Create default config
37
+ const defaultConfig = {
38
+ port: DEFAULT_PORT,
39
+ logLevel: 'info',
40
+ autoStart: true
41
+ };
42
+ saveConfig(defaultConfig);
43
+ return defaultConfig;
44
+ }
45
+
46
+ try {
47
+ const content = fs.readFileSync(configPath, 'utf8');
48
+ return JSON.parse(content);
49
+ } catch (error) {
50
+ console.error('Failed to load config:', error.message);
51
+ return { port: DEFAULT_PORT };
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Save config to file
57
+ */
58
+ function saveConfig(config) {
59
+ ensureConfigDir();
60
+
61
+ const configPath = getConfigPath();
62
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
63
+ }
64
+
65
+ /**
66
+ * Get port from config
67
+ */
68
+ function getPort() {
69
+ const config = loadConfig();
70
+ const port = config.port || DEFAULT_PORT;
71
+ // Ensure port is a valid integer
72
+ const portNum = parseInt(port, 10);
73
+ if (isNaN(portNum) || portNum <= 0 || portNum > 65535) {
74
+ console.warn(`Invalid port ${port}, using default ${DEFAULT_PORT}`);
75
+ return DEFAULT_PORT;
76
+ }
77
+ return portNum;
78
+ }
79
+
80
+ /**
81
+ * Set port in config
82
+ */
83
+ function setPort(port) {
84
+ const config = loadConfig();
85
+ // Ensure port is stored as integer
86
+ const portNum = parseInt(port, 10);
87
+ if (isNaN(portNum) || portNum <= 0 || portNum > 65535) {
88
+ throw new Error(`Invalid port number: ${port}`);
89
+ }
90
+ config.port = portNum;
91
+ saveConfig(config);
92
+ }
93
+
94
+ module.exports = {
95
+ ensureConfigDir,
96
+ loadConfig,
97
+ saveConfig,
98
+ getPort,
99
+ setPort,
100
+ getConfigPath, // Re-export from platform.js
101
+ DEFAULT_PORT
102
+ };
103
+
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Daemon process management
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const { spawn, execSync } = require('child_process');
7
+ const http = require('http');
8
+ const { getDaemonBinaryPath, getPidPath, getLogPath } = require('./platform');
9
+ const { getPort, getConfigPath } = require('./config');
10
+
11
+ /**
12
+ * Check if daemon is running
13
+ */
14
+ function isRunning() {
15
+ const pidPath = getPidPath();
16
+
17
+ if (!fs.existsSync(pidPath)) {
18
+ return false;
19
+ }
20
+
21
+ try {
22
+ const pid = parseInt(fs.readFileSync(pidPath, 'utf8').trim());
23
+
24
+ // Check if process exists
25
+ process.kill(pid, 0);
26
+ return true;
27
+ } catch (error) {
28
+ // Process doesn't exist
29
+ return false;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Get daemon health status
35
+ */
36
+ function getHealth() {
37
+ return new Promise((resolve) => {
38
+ const port = getPort();
39
+ const options = {
40
+ hostname: 'localhost',
41
+ port: port,
42
+ path: '/health',
43
+ method: 'GET',
44
+ timeout: 2000
45
+ };
46
+
47
+ const req = http.request(options, (res) => {
48
+ let data = '';
49
+ res.on('data', (chunk) => { data += chunk; });
50
+ res.on('end', () => {
51
+ try {
52
+ const health = JSON.parse(data);
53
+ resolve(health);
54
+ } catch (error) {
55
+ resolve(null);
56
+ }
57
+ });
58
+ });
59
+
60
+ req.on('error', () => resolve(null));
61
+ req.on('timeout', () => {
62
+ req.destroy();
63
+ resolve(null);
64
+ });
65
+
66
+ req.end();
67
+ });
68
+ }
69
+
70
+ /**
71
+ * Start daemon
72
+ */
73
+ function start() {
74
+ if (isRunning()) {
75
+ throw new Error('Daemon is already running');
76
+ }
77
+
78
+ const binaryPath = getDaemonBinaryPath();
79
+ if (!fs.existsSync(binaryPath)) {
80
+ throw new Error(
81
+ `Daemon binary not found at: ${binaryPath}\n` +
82
+ `This might be a corrupted installation. Try reinstalling:\n` +
83
+ ` npm install -g @ringcentral/ai-desk-daemon --force`
84
+ );
85
+ }
86
+
87
+ // Ensure binary is executable (Unix-like systems)
88
+ if (process.platform !== 'win32') {
89
+ try {
90
+ fs.chmodSync(binaryPath, 0o755);
91
+ } catch (error) {
92
+ // Ignore permission errors
93
+ }
94
+ }
95
+
96
+ const port = getPort();
97
+ const configPath = getConfigPath();
98
+ const logPath = getLogPath();
99
+
100
+ // Ensure port is a number
101
+ const portNumber = typeof port === 'number' ? port : parseInt(port, 10);
102
+ if (isNaN(portNumber)) {
103
+ throw new Error(`Invalid port number: ${port}`);
104
+ }
105
+
106
+ // Ensure log directory exists
107
+ const logDir = require('path').dirname(logPath);
108
+ if (!fs.existsSync(logDir)) {
109
+ fs.mkdirSync(logDir, { recursive: true });
110
+ }
111
+
112
+ // The daemon handles its own logging to file (see daemon/logger.go)
113
+ // It uses io.MultiWriter to write to both stdout and the log file
114
+ // So we should ignore stdout/stderr here to avoid duplication
115
+ const child = spawn(binaryPath, ['--port', portNumber.toString(), '--config', configPath], {
116
+ detached: true,
117
+ stdio: ['ignore', 'ignore', 'ignore'] // Daemon writes to its own log file
118
+ });
119
+
120
+ child.unref();
121
+
122
+ // Save PID
123
+ fs.writeFileSync(getPidPath(), child.pid.toString());
124
+
125
+ console.log(`Daemon started (PID: ${child.pid})`);
126
+ console.log(`Port: ${portNumber}`);
127
+ console.log(`Logs: ${logPath}`);
128
+
129
+ return child.pid;
130
+ }
131
+
132
+ /**
133
+ * Stop daemon
134
+ */
135
+ function stop() {
136
+ const pidPath = getPidPath();
137
+
138
+ if (!fs.existsSync(pidPath)) {
139
+ throw new Error('Daemon is not running');
140
+ }
141
+
142
+ const pid = parseInt(fs.readFileSync(pidPath, 'utf8').trim());
143
+
144
+ try {
145
+ process.kill(pid, 'SIGTERM');
146
+ fs.unlinkSync(pidPath);
147
+ console.log('Daemon stopped');
148
+ } catch (error) {
149
+ fs.unlinkSync(pidPath);
150
+ throw new Error(`Failed to stop daemon: ${error.message}`);
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Restart daemon
156
+ */
157
+ function restart() {
158
+ if (isRunning()) {
159
+ stop();
160
+ // Wait a bit for graceful shutdown
161
+ execSync('sleep 1');
162
+ }
163
+ start();
164
+ }
165
+
166
+ /**
167
+ * Get daemon status
168
+ */
169
+ async function status() {
170
+ const running = isRunning();
171
+ const health = running ? await getHealth() : null;
172
+
173
+ return {
174
+ running,
175
+ health,
176
+ port: getPort(),
177
+ logPath: getLogPath()
178
+ };
179
+ }
180
+
181
+ module.exports = {
182
+ isRunning,
183
+ getHealth,
184
+ start,
185
+ stop,
186
+ restart,
187
+ status
188
+ };
189
+
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Platform detection and binary path resolution
3
+ */
4
+
5
+ const os = require('os');
6
+ const path = require('path');
7
+
8
+ const VERSION = '1.0.17';
9
+
10
+ /**
11
+ * Detect current platform and architecture
12
+ */
13
+ function detectPlatform() {
14
+ const platform = os.platform();
15
+ const arch = os.arch();
16
+
17
+ let platformKey;
18
+
19
+ // Map Node.js platform to binary directory name
20
+ switch (platform) {
21
+ case 'darwin':
22
+ platformKey = arch === 'arm64' ? 'darwin-arm64' : 'darwin-x64';
23
+ break;
24
+ case 'linux':
25
+ platformKey = arch === 'arm64' ? 'linux-arm64' : 'linux-x64';
26
+ break;
27
+ case 'win32':
28
+ platformKey = 'win32-x64';
29
+ break;
30
+ default:
31
+ throw new Error(`Unsupported platform: ${platform}-${arch}`);
32
+ }
33
+
34
+ return { platform, arch, platformKey };
35
+ }
36
+
37
+ /**
38
+ * Get daemon binary path from npm package
39
+ */
40
+ function getDaemonBinaryPath() {
41
+ const { platform, platformKey } = detectPlatform();
42
+
43
+ // Binary is in the npm package under dist/<platform>/
44
+ const binaryName = platform === 'win32' ? 'ai-desk-daemon.exe' : 'ai-desk-daemon';
45
+ const binaryPath = path.join(__dirname, '..', 'dist', platformKey, binaryName);
46
+
47
+ return binaryPath;
48
+ }
49
+
50
+ /**
51
+ * Get config directory
52
+ */
53
+ function getConfigDir() {
54
+ return path.join(os.homedir(), '.aidesktop');
55
+ }
56
+
57
+ /**
58
+ * Get config file path
59
+ */
60
+ function getConfigPath() {
61
+ return path.join(getConfigDir(), 'daemon-config.json');
62
+ }
63
+
64
+ /**
65
+ * Get log file path
66
+ */
67
+ function getLogPath() {
68
+ return path.join(getConfigDir(), 'logs', 'daemon.log');
69
+ }
70
+
71
+ /**
72
+ * Get PID file path
73
+ */
74
+ function getPidPath() {
75
+ return path.join(getConfigDir(), 'daemon.pid');
76
+ }
77
+
78
+ module.exports = {
79
+ detectPlatform,
80
+ getDaemonBinaryPath,
81
+ getConfigDir,
82
+ getConfigPath,
83
+ getLogPath,
84
+ getPidPath,
85
+ VERSION
86
+ };
87
+
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@agent-webui/ai-desk-daemon",
3
+ "version": "1.0.17",
4
+ "description": "AI Desk Daemon - CLI tool for managing the AI Desk daemon service",
5
+ "main": "lib/daemon-manager.js",
6
+ "bin": {
7
+ "aidesk": "bin/cli.js"
8
+ },
9
+ "scripts": {
10
+ "postinstall": "node scripts/postinstall.js",
11
+ "test": "node bin/cli.js --help"
12
+ },
13
+ "keywords": [
14
+ "ai-desk",
15
+ "daemon",
16
+ "cli",
17
+ "agent-webui"
18
+ ],
19
+ "author": "Agent WebUI",
20
+ "license": "MIT",
21
+ "engines": {
22
+ "node": ">=14.0.0"
23
+ },
24
+ "files": [
25
+ "bin/",
26
+ "lib/",
27
+ "scripts/postinstall.js",
28
+ "README.md"
29
+ ],
30
+ "dependencies": {
31
+ "commander": "^11.0.0",
32
+ "chalk": "^4.1.2"
33
+ },
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/agent-webui/ai-desk-daemon.git"
37
+ },
38
+ "homepage": "https://github.com/agent-webui",
39
+ "publishConfig": {
40
+ "access": "public"
41
+ }
42
+ }
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Post-install script to download platform-specific daemon binary from GitHub Releases
5
+ */
6
+
7
+ const https = require('https');
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const { execSync } = require('child_process');
11
+
12
+ const GITHUB_REPO = 'agent-webui/ai-desk-daemon';
13
+ const PACKAGE_VERSION = require('../package.json').version;
14
+
15
+ // Platform mapping
16
+ const PLATFORM_MAP = {
17
+ 'darwin-x64': 'darwin-x64',
18
+ 'darwin-arm64': 'darwin-arm64',
19
+ 'linux-x64': 'linux-x64',
20
+ 'linux-arm64': 'linux-arm64',
21
+ 'win32-x64': 'win32-x64'
22
+ };
23
+
24
+ function getPlatform() {
25
+ const platform = process.platform;
26
+ const arch = process.arch;
27
+ const key = `${platform}-${arch}`;
28
+
29
+ if (!PLATFORM_MAP[key]) {
30
+ console.error(`Unsupported platform: ${platform}-${arch}`);
31
+ console.error('Supported platforms:', Object.keys(PLATFORM_MAP).join(', '));
32
+ process.exit(1);
33
+ }
34
+
35
+ return PLATFORM_MAP[key];
36
+ }
37
+
38
+ function getBinaryName(platform) {
39
+ return platform.startsWith('win32') ? 'ai-desk-daemon.exe' : 'ai-desk-daemon';
40
+ }
41
+
42
+ function downloadFile(url, dest) {
43
+ return new Promise((resolve, reject) => {
44
+ console.log(`Downloading from: ${url}`);
45
+
46
+ const file = fs.createWriteStream(dest);
47
+
48
+ https.get(url, (response) => {
49
+ if (response.statusCode === 302 || response.statusCode === 301) {
50
+ // Follow redirect
51
+ file.close();
52
+ fs.unlinkSync(dest);
53
+ return downloadFile(response.headers.location, dest).then(resolve).catch(reject);
54
+ }
55
+
56
+ if (response.statusCode !== 200) {
57
+ file.close();
58
+ fs.unlinkSync(dest);
59
+ return reject(new Error(`Failed to download: HTTP ${response.statusCode}`));
60
+ }
61
+
62
+ const totalSize = parseInt(response.headers['content-length'], 10);
63
+ let downloadedSize = 0;
64
+ let lastPercent = 0;
65
+
66
+ response.on('data', (chunk) => {
67
+ downloadedSize += chunk.length;
68
+ const percent = Math.floor((downloadedSize / totalSize) * 100);
69
+ if (percent > lastPercent && percent % 10 === 0) {
70
+ console.log(`Progress: ${percent}%`);
71
+ lastPercent = percent;
72
+ }
73
+ });
74
+
75
+ response.pipe(file);
76
+
77
+ file.on('finish', () => {
78
+ file.close();
79
+ console.log('Download complete!');
80
+ resolve();
81
+ });
82
+ }).on('error', (err) => {
83
+ file.close();
84
+ fs.unlinkSync(dest);
85
+ reject(err);
86
+ });
87
+ });
88
+ }
89
+
90
+ async function main() {
91
+ console.log('='.repeat(50));
92
+ console.log('AI Desk Daemon - Post Install');
93
+ console.log('='.repeat(50));
94
+
95
+ const platform = getPlatform();
96
+ const binaryName = getBinaryName(platform);
97
+
98
+ console.log(`Platform: ${platform}`);
99
+ console.log(`Version: ${PACKAGE_VERSION}`);
100
+ console.log(`Binary: ${binaryName}`);
101
+ console.log('');
102
+
103
+ // Create dist directory
104
+ const distDir = path.join(__dirname, '..', 'dist', platform);
105
+ if (!fs.existsSync(distDir)) {
106
+ fs.mkdirSync(distDir, { recursive: true });
107
+ }
108
+
109
+ const binaryPath = path.join(distDir, binaryName);
110
+
111
+ // Check if binary already exists
112
+ if (fs.existsSync(binaryPath)) {
113
+ console.log('Binary already exists, skipping download.');
114
+ console.log(`Location: ${binaryPath}`);
115
+ return;
116
+ }
117
+
118
+ // Download from GitHub Releases
119
+ // Format: ai-desk-daemon-darwin-arm64 or ai-desk-daemon-win32-x64.exe
120
+ const assetName = platform.startsWith('win32')
121
+ ? `ai-desk-daemon-${platform}.exe`
122
+ : `ai-desk-daemon-${platform}`;
123
+ const downloadUrl = `https://github.com/${GITHUB_REPO}/releases/download/v${PACKAGE_VERSION}/${assetName}`;
124
+
125
+ console.log('Downloading daemon binary...');
126
+
127
+ try {
128
+ await downloadFile(downloadUrl, binaryPath);
129
+
130
+ // Set executable permission (Unix-like systems)
131
+ if (process.platform !== 'win32') {
132
+ fs.chmodSync(binaryPath, 0o755);
133
+ console.log('Set executable permission');
134
+ }
135
+
136
+ console.log('');
137
+ console.log('✓ Installation complete!');
138
+ console.log(`Binary installed at: ${binaryPath}`);
139
+ console.log('');
140
+ console.log('Run "aidesk --help" to get started.');
141
+ console.log('='.repeat(50));
142
+ } catch (error) {
143
+ console.error('');
144
+ console.error('✗ Failed to download daemon binary');
145
+ console.error(`Error: ${error.message}`);
146
+ console.error('');
147
+ console.error('Please try one of the following:');
148
+ console.error(`1. Download manually from: https://github.com/${GITHUB_REPO}/releases/tag/v${PACKAGE_VERSION}`);
149
+ console.error(`2. Place the binary at: ${binaryPath}`);
150
+ console.error('3. Report this issue at: https://github.com/${GITHUB_REPO}/issues');
151
+ console.error('='.repeat(50));
152
+ process.exit(1);
153
+ }
154
+ }
155
+
156
+ main();
157
+