@epic-cloudcontrol/daemon 0.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.
package/README.md ADDED
@@ -0,0 +1,150 @@
1
+ # @epic-cloudcontrol/daemon
2
+
3
+ Local daemon for [CloudControl](https://github.com/Epic-Design-Labs/app-cloudcontrol) — connects your machine to the cloud control plane. AI agents and humans pull tasks, execute them locally, and report results.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g @epic-cloudcontrol/daemon
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```bash
14
+ # 1. Log in (syncs all your companies)
15
+ cloudcontrol login --email you@example.com
16
+
17
+ # 2. Install as auto-start service (starts on boot)
18
+ cloudcontrol install
19
+
20
+ # 3. Verify
21
+ cloudcontrol status
22
+ ```
23
+
24
+ Done. The daemon runs in the background, polls all your companies for tasks, and executes them via AI.
25
+
26
+ ## Commands
27
+
28
+ | Command | Description |
29
+ |---------|-------------|
30
+ | `cloudcontrol login` | Log in with email, sync all company profiles |
31
+ | `cloudcontrol profiles` | List saved company profiles |
32
+ | `cloudcontrol refresh` | Discover new companies (no password needed) |
33
+ | `cloudcontrol models` | List available AI models on this machine |
34
+ | `cloudcontrol status` | Show config, connection status, service status |
35
+ | `cloudcontrol start` | Start daemon for one profile (foreground) |
36
+ | `cloudcontrol start --all` | Start daemon for ALL profiles (foreground) |
37
+ | `cloudcontrol install` | Install as system service (auto-start on boot) |
38
+ | `cloudcontrol uninstall` | Remove system service |
39
+ | `cloudcontrol logs` | View daemon logs (`--follow`, `--errors`, `-n 100`) |
40
+ | `cloudcontrol mcp` | Start MCP server for Claude Desktop |
41
+
42
+ ## Claude Desktop Integration
43
+
44
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or equivalent:
45
+
46
+ ```json
47
+ {
48
+ "mcpServers": {
49
+ "cloudcontrol": {
50
+ "command": "cloudcontrol",
51
+ "args": ["mcp"]
52
+ }
53
+ }
54
+ }
55
+ ```
56
+
57
+ For a specific company:
58
+
59
+ ```json
60
+ {
61
+ "mcpServers": {
62
+ "cloudcontrol": {
63
+ "command": "cloudcontrol",
64
+ "args": ["mcp", "--profile", "my-company"]
65
+ }
66
+ }
67
+ }
68
+ ```
69
+
70
+ ## Multi-Company
71
+
72
+ The daemon supports multiple companies from one machine:
73
+
74
+ ```bash
75
+ # Login syncs all companies you belong to
76
+ cloudcontrol login --email you@example.com
77
+
78
+ # See them
79
+ cloudcontrol profiles
80
+
81
+ # Run all at once
82
+ cloudcontrol start --all
83
+
84
+ # Or pick one
85
+ cloudcontrol start --profile my-company
86
+ ```
87
+
88
+ When installed as a service (`cloudcontrol install`), it runs `--all` automatically.
89
+
90
+ New companies added in the dashboard are auto-discovered on daemon restart, or manually via:
91
+
92
+ ```bash
93
+ cloudcontrol refresh
94
+ ```
95
+
96
+ ## AI Models
97
+
98
+ The daemon auto-detects available AI models:
99
+
100
+ | Source | Models | Setup |
101
+ |--------|--------|-------|
102
+ | Claude API | claude-sonnet, claude-haiku | `export ANTHROPIC_API_KEY=sk-ant-...` |
103
+ | Claude Code | claude-code | Install: `npm i -g @anthropic-ai/claude-code` |
104
+ | Gemini | gemini | Install: `npm i -g @google/gemini-cli` |
105
+ | Ollama | llama3, mistral, etc. | `export CLOUDCONTROL_OLLAMA_MODELS=llama3,mistral` |
106
+ | Custom CLI | any | `export CLOUDCONTROL_CLI_MODELS="name:binary:args"` |
107
+
108
+ Check what's available:
109
+
110
+ ```bash
111
+ cloudcontrol models
112
+ ```
113
+
114
+ ## Auto-Start Details
115
+
116
+ `cloudcontrol install` creates a platform-specific service:
117
+
118
+ | Platform | Mechanism | Config Location |
119
+ |----------|-----------|----------------|
120
+ | macOS | LaunchAgent | `~/Library/LaunchAgents/com.cloudcontrol.daemon.plist` |
121
+ | Linux | systemd user service | `~/.config/systemd/user/cloudcontrol-daemon.service` |
122
+ | Windows | Scheduled Task | Task: `CloudControlDaemon` |
123
+
124
+ Logs are written to `~/.cloudcontrol/logs/`. View with:
125
+
126
+ ```bash
127
+ cloudcontrol logs # stdout
128
+ cloudcontrol logs --errors # stderr
129
+ cloudcontrol logs --follow # tail -f
130
+ ```
131
+
132
+ ## Configuration
133
+
134
+ All config is stored in `~/.cloudcontrol/`:
135
+
136
+ ```
137
+ ~/.cloudcontrol/
138
+ ├── config.json # default profile
139
+ ├── profiles/
140
+ │ ├── my-company.json # named profile
141
+ │ └── other-company.json
142
+ └── logs/
143
+ ├── daemon.log
144
+ └── daemon.err
145
+ ```
146
+
147
+ ## Requirements
148
+
149
+ - Node.js 20+
150
+ - A CloudControl account (self-hosted or cloud)
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,525 @@
1
+ #!/usr/bin/env node
2
+ import { config } from "dotenv";
3
+ import path from "path";
4
+ import { fileURLToPath } from "url";
5
+ // Load .env.local from project root (for local dev)
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ config({ path: path.resolve(__dirname, "../../.env.local") });
8
+ import { Command } from "commander";
9
+ import os from "os";
10
+ import readline from "readline";
11
+ import { execSync } from "child_process";
12
+ import { existsSync } from "fs";
13
+ import { loadConfig } from "./config.js";
14
+ import { loadProfile, saveProfile, profileExists, listProfiles, getConfigDir } from "./profile.js";
15
+ import { TaskExecutor } from "./task-executor.js";
16
+ import { ModelRouter } from "./model-router.js";
17
+ import { fetchWithRetry } from "./retry.js";
18
+ import { DAEMON_VERSION } from "./version.js";
19
+ import { install, uninstall, serviceStatus, getLogPath, getErrorLogPath, detectPlatform } from "./service-manager.js";
20
+ const program = new Command();
21
+ program
22
+ .name("cloudcontrol")
23
+ .description("CloudControl workstation daemon — connects your machine to the cloud control plane")
24
+ .version(DAEMON_VERSION);
25
+ // ── login ─────────────────────────────────────────────
26
+ program
27
+ .command("login")
28
+ .description("Log in and sync all your company profiles")
29
+ .option("--email <email>", "Log in with email (syncs all companies automatically)")
30
+ .option("--profile <name>", "Profile name for single-key setup", "default")
31
+ .option("--api-url <url>", "CloudControl API URL")
32
+ .option("--api-key <key>", "API key (for single-company manual setup)")
33
+ .option("--name <name>", "Worker name")
34
+ .action(async (opts) => {
35
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
36
+ const ask = (q, defaultVal) => new Promise((resolve) => {
37
+ const prompt = defaultVal ? `${q} [${defaultVal}]: ` : `${q}: `;
38
+ rl.question(prompt, (answer) => resolve(answer.trim() || defaultVal || ""));
39
+ });
40
+ let apiUrl = opts.apiUrl;
41
+ if (!apiUrl) {
42
+ const existing = loadProfile();
43
+ apiUrl = await ask("CloudControl API URL", existing?.apiUrl || "https://cloudcontrol.onrender.com");
44
+ }
45
+ // Email login mode — sync all companies
46
+ const email = opts.email || await ask("Email (or leave blank for API key setup)");
47
+ if (email) {
48
+ const password = await ask("Password");
49
+ rl.close();
50
+ if (!password) {
51
+ console.error("Error: Password required.");
52
+ process.exit(1);
53
+ }
54
+ console.log("\nAuthenticating...");
55
+ try {
56
+ const res = await fetch(`${apiUrl}/api/auth/teams`, {
57
+ method: "POST",
58
+ headers: { "content-type": "application/json" },
59
+ body: JSON.stringify({ email, password, generateKeys: true }),
60
+ });
61
+ if (!res.ok) {
62
+ const data = await res.json();
63
+ console.error(`Login failed: ${data.error || res.status}`);
64
+ process.exit(1);
65
+ }
66
+ const data = await res.json();
67
+ const workerName = opts.name || `worker-${os.hostname()}`;
68
+ console.log(`\nLogged in as ${data.user.email}`);
69
+ console.log(`Found ${data.teams.length} company(s):\n`);
70
+ for (const team of data.teams) {
71
+ // Generate slug from team name
72
+ const slug = team.teamName
73
+ .toLowerCase()
74
+ .replace(/[^a-z0-9]+/g, "-")
75
+ .replace(/^-|-$/g, "");
76
+ saveProfile({
77
+ apiUrl,
78
+ apiKey: team.apiKey,
79
+ workerName,
80
+ teamName: team.teamName,
81
+ }, slug);
82
+ console.log(` ✓ ${team.teamName} → profile "${slug}" (${team.role})`);
83
+ }
84
+ // Also save the first team as default
85
+ if (data.teams.length > 0) {
86
+ const first = data.teams[0];
87
+ saveProfile({
88
+ apiUrl,
89
+ apiKey: first.apiKey,
90
+ workerName,
91
+ teamName: first.teamName,
92
+ });
93
+ }
94
+ console.log(`\n${data.teams.length} profile(s) saved to ${getConfigDir()}/`);
95
+ console.log("Run 'cloudcontrol profiles' to see them.");
96
+ console.log("Run 'cloudcontrol start --profile <name>' to start working.");
97
+ }
98
+ catch (err) {
99
+ console.error(`Error: ${err.message}`);
100
+ process.exit(1);
101
+ }
102
+ }
103
+ else {
104
+ // Manual API key mode (original flow)
105
+ const profileName = opts.profile;
106
+ let apiKey = opts.apiKey;
107
+ let workerName = opts.name;
108
+ let teamName;
109
+ const existing = loadProfile(profileName);
110
+ if (!teamName)
111
+ teamName = await ask("Company name", existing?.teamName || profileName);
112
+ if (!apiKey)
113
+ apiKey = await ask("API Key (cc_...)", existing?.apiKey);
114
+ if (!workerName)
115
+ workerName = await ask("Worker name", existing?.workerName || `worker-${os.hostname()}`);
116
+ rl.close();
117
+ if (!apiKey) {
118
+ console.error("Error: API key is required.");
119
+ process.exit(1);
120
+ }
121
+ console.log("\nVerifying connection...");
122
+ try {
123
+ const res = await fetch(`${apiUrl}/api/health`);
124
+ if (!res.ok)
125
+ throw new Error(`HTTP ${res.status}`);
126
+ console.log("Connected to CloudControl.");
127
+ }
128
+ catch (err) {
129
+ console.error(`Warning: Could not reach ${apiUrl} — ${err.message}`);
130
+ }
131
+ saveProfile({ apiUrl, apiKey, workerName, teamName }, profileName);
132
+ console.log(`\nProfile "${profileName}" saved to ${getConfigDir()}/`);
133
+ console.log("Run 'cloudcontrol start' to begin processing tasks.");
134
+ }
135
+ });
136
+ // ── profiles ──────────────────────────────────────────
137
+ program
138
+ .command("profiles")
139
+ .description("List all saved company profiles")
140
+ .action(() => {
141
+ const profiles = listProfiles();
142
+ if (profiles.length === 0) {
143
+ console.log("No profiles saved. Run 'cloudcontrol login' to create one.");
144
+ return;
145
+ }
146
+ console.log("\nSaved profiles:\n");
147
+ console.log(" Name Company API URL");
148
+ console.log(" ────────────────── ─────────────────── ──────────────────────────");
149
+ for (const { name, profile } of profiles) {
150
+ const teamName = profile.teamName || "—";
151
+ console.log(` ${name.padEnd(20)}${teamName.padEnd(21)}${profile.apiUrl}`);
152
+ }
153
+ console.log("");
154
+ console.log("Use: cloudcontrol start --profile <name>");
155
+ });
156
+ // ── refresh ──────────────────────────────────────────
157
+ program
158
+ .command("refresh")
159
+ .description("Discover new companies using your existing API key (no password needed)")
160
+ .option("--profile <name>", "Profile to use as the source key", "default")
161
+ .action(async (opts) => {
162
+ const profile = loadProfile(opts.profile);
163
+ if (!profile?.apiKey) {
164
+ console.error("Error: No API key found. Run 'cloudcontrol login' first.");
165
+ process.exit(1);
166
+ }
167
+ console.log("Checking for new companies...\n");
168
+ try {
169
+ const res = await fetch(`${profile.apiUrl}/api/auth/teams?generateKeys=true`, {
170
+ headers: { authorization: `Bearer ${profile.apiKey}` },
171
+ });
172
+ if (!res.ok) {
173
+ const data = await res.json().catch(() => ({}));
174
+ console.error(`Failed: ${data.error || res.status}`);
175
+ process.exit(1);
176
+ }
177
+ const data = await res.json();
178
+ const existing = new Set(listProfiles().map((p) => p.name));
179
+ const workerName = profile.workerName || `worker-${os.hostname()}`;
180
+ let newCount = 0;
181
+ for (const team of data.teams) {
182
+ const slug = team.teamName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
183
+ if (!existing.has(slug) && team.apiKey) {
184
+ saveProfile({
185
+ apiUrl: profile.apiUrl,
186
+ apiKey: team.apiKey,
187
+ workerName,
188
+ teamName: team.teamName,
189
+ }, slug);
190
+ console.log(` ✓ New: ${team.teamName} → profile "${slug}" (${team.role})`);
191
+ newCount++;
192
+ }
193
+ else {
194
+ console.log(` ${team.teamName} → already synced`);
195
+ }
196
+ }
197
+ if (newCount > 0) {
198
+ console.log(`\n${newCount} new company(s) added.`);
199
+ }
200
+ else {
201
+ console.log(`\nAll ${data.teams.length} company(s) already synced.`);
202
+ }
203
+ console.log("Run 'cloudcontrol profiles' to see them.");
204
+ }
205
+ catch (err) {
206
+ console.error(`Error: ${err.message}`);
207
+ process.exit(1);
208
+ }
209
+ });
210
+ // ── install ──────────────────────────────────────────
211
+ program
212
+ .command("install")
213
+ .description("Install daemon as a system service (auto-starts on login)")
214
+ .action(() => {
215
+ const platform = detectPlatform();
216
+ console.log(`\nPlatform: ${platform}`);
217
+ console.log("Installing CloudControl daemon as a background service...\n");
218
+ const profiles = listProfiles();
219
+ if (profiles.length === 0) {
220
+ console.error("Error: No profiles found. Run 'cloudcontrol login' first.");
221
+ process.exit(1);
222
+ }
223
+ console.log(`Will run ${profiles.length} company(s):`);
224
+ for (const { name, profile } of profiles) {
225
+ console.log(` - ${profile.teamName || name}`);
226
+ }
227
+ console.log("");
228
+ const result = install();
229
+ if (result.success) {
230
+ console.log(`Done! ${result.message}`);
231
+ }
232
+ else {
233
+ console.error(`Error: ${result.message}`);
234
+ process.exit(1);
235
+ }
236
+ });
237
+ // ── uninstall ────────────────────────────────────────
238
+ program
239
+ .command("uninstall")
240
+ .description("Remove daemon system service (stops auto-start)")
241
+ .action(() => {
242
+ const result = uninstall();
243
+ console.log(result.message);
244
+ if (!result.success)
245
+ process.exit(1);
246
+ });
247
+ // ── logs ─────────────────────────────────────────────
248
+ program
249
+ .command("logs")
250
+ .description("Show daemon service logs")
251
+ .option("--errors", "Show error log instead of stdout")
252
+ .option("--follow", "Follow log output (tail -f)")
253
+ .option("-n, --lines <count>", "Number of lines to show", "50")
254
+ .action((opts) => {
255
+ const logPath = opts.errors ? getErrorLogPath() : getLogPath();
256
+ const platform = detectPlatform();
257
+ // Linux: prefer journalctl
258
+ if (platform === "linux" && !opts.errors) {
259
+ try {
260
+ const args = opts.follow ? "-f" : `-n ${opts.lines}`;
261
+ execSync(`journalctl --user -u cloudcontrol-daemon ${args}`, { stdio: "inherit" });
262
+ return;
263
+ }
264
+ catch { /* fallback to file */ }
265
+ }
266
+ if (!existsSync(logPath)) {
267
+ console.log(`No log file at ${logPath}`);
268
+ console.log("The daemon may not have been installed yet. Run 'cloudcontrol install'.");
269
+ return;
270
+ }
271
+ const cmd = opts.follow ? `tail -f "${logPath}"` : `tail -n ${opts.lines} "${logPath}"`;
272
+ try {
273
+ execSync(cmd, { stdio: "inherit" });
274
+ }
275
+ catch {
276
+ console.error(`Could not read log file: ${logPath}`);
277
+ }
278
+ });
279
+ // ── models ────────────────────────────────────────────
280
+ program
281
+ .command("models")
282
+ .description("List available AI models on this machine")
283
+ .option("--profile <name>", "Profile to use", "default")
284
+ .action((opts) => {
285
+ const router = new ModelRouter();
286
+ const models = router.listModels();
287
+ if (models.length === 0) {
288
+ console.log("No models available.");
289
+ console.log("Install a CLI (claude, gemini, codex), set ANTHROPIC_API_KEY, or configure Ollama.");
290
+ return;
291
+ }
292
+ console.log("\nAvailable models:\n");
293
+ console.log(" Name Traits");
294
+ console.log(" ────────────────── ──────────────────────────");
295
+ for (const m of models) {
296
+ console.log(` ${m.name.padEnd(20)}${m.traits.join(", ")}`);
297
+ }
298
+ console.log("");
299
+ console.log(`Default: ${router.getDefault()}`);
300
+ console.log("");
301
+ console.log("Add models:");
302
+ console.log(" Install a CLI: npm install -g @google/gemini-cli");
303
+ console.log(" API key: export ANTHROPIC_API_KEY=sk-ant-...");
304
+ console.log(" Ollama: export CLOUDCONTROL_OLLAMA_MODELS=llama3,mistral");
305
+ console.log(" Custom CLI: export CLOUDCONTROL_CLI_MODELS=\"mycli:mybinary:-p\"");
306
+ });
307
+ // ── status ────────────────────────────────────────────
308
+ program
309
+ .command("status")
310
+ .description("Check connection to CloudControl and show config")
311
+ .option("--profile <name>", "Profile to use", "default")
312
+ .action(async (opts) => {
313
+ const profile = loadProfile(opts.profile);
314
+ const cfg = loadConfig({ profileName: opts.profile });
315
+ console.log("\nConfiguration:");
316
+ console.log(` Config file: ${profileExists() ? getConfigDir() + "/config.json" : "(not saved — using env vars)"}`);
317
+ console.log(` API URL: ${cfg.apiUrl}`);
318
+ console.log(` API Key: ${cfg.apiKey ? cfg.apiKey.slice(0, 11) + "..." : "(not set)"}`);
319
+ console.log(` Worker name: ${cfg.workerName}`);
320
+ console.log(` Platform: ${os.platform()}/${os.arch()}`);
321
+ if (!cfg.apiKey) {
322
+ console.log("\nNot configured. Run 'cloudcontrol login' first.");
323
+ return;
324
+ }
325
+ // Check cloud connection
326
+ console.log("\nCloud connection:");
327
+ try {
328
+ const res = await fetch(`${cfg.apiUrl}/api/health`);
329
+ const data = await res.json();
330
+ console.log(` Status: ${data.status}`);
331
+ if (data.coordinator)
332
+ console.log(` Coordinator: ${data.coordinator.running ? "running" : "stopped"}`);
333
+ if (data.scheduler)
334
+ console.log(` Scheduler: ${data.scheduler.running ? "running" : "stopped"}`);
335
+ }
336
+ catch (err) {
337
+ console.log(` Error: Could not reach ${cfg.apiUrl}`);
338
+ }
339
+ // Show models
340
+ const router = new ModelRouter();
341
+ const models = router.listModels();
342
+ console.log(`\nModels: ${models.map((m) => m.name).join(", ") || "none"}`);
343
+ console.log(`Default: ${router.getDefault()}`);
344
+ console.log(`\nDaemon service: ${serviceStatus()}`);
345
+ });
346
+ // ── mcp ──────────────────────────────────────────────
347
+ program
348
+ .command("mcp")
349
+ .description("Start the MCP server for Claude Desktop (stdio transport)")
350
+ .option("--profile <name>", "Company profile to use")
351
+ .action(async (opts) => {
352
+ if (opts.profile)
353
+ process.env.CLOUDCONTROL_PROFILE = opts.profile;
354
+ await import("./mcp-server.js");
355
+ });
356
+ // ── start ─────────────────────────────────────────────
357
+ program
358
+ .command("start")
359
+ .description("Start the daemon and begin processing tasks")
360
+ .option("--profile <name>", "Company profile to use", "default")
361
+ .option("--api-url <url>", "CloudControl API URL (overrides profile)")
362
+ .option("--api-key <key>", "API key (overrides profile)")
363
+ .option("--name <name>", "Worker name")
364
+ .option("--worker-type <type>", "Worker type (daemon, cli, ide)", "daemon")
365
+ .option("--capabilities <list>", "Comma-separated capabilities", "browser,filesystem,shell,ai_execution")
366
+ .option("--task-types <list>", "Comma-separated task types to accept (empty = all)")
367
+ .option("--poll-interval <ms>", "Poll interval in milliseconds", "15000")
368
+ .option("--model <model>", "Override default model for all tasks")
369
+ .option("--all", "Start workers for ALL saved company profiles")
370
+ .action(async (opts) => {
371
+ if (opts.all) {
372
+ const { startAllProfiles } = await import("./multi-profile.js");
373
+ const shutdown = await startAllProfiles({
374
+ workerType: opts.workerType,
375
+ capabilities: opts.capabilities?.split(",").map((s) => s.trim()),
376
+ pollInterval: parseInt(opts.pollInterval),
377
+ model: opts.model,
378
+ });
379
+ const stop = () => {
380
+ console.log("\n[daemon] Shutting down all workers...");
381
+ shutdown();
382
+ process.exit(0);
383
+ };
384
+ process.on("SIGINT", stop);
385
+ process.on("SIGTERM", stop);
386
+ return;
387
+ }
388
+ const cfg = loadConfig({
389
+ profileName: opts.profile,
390
+ apiUrl: opts.apiUrl,
391
+ apiKey: opts.apiKey,
392
+ workerName: opts.name,
393
+ workerType: opts.workerType,
394
+ capabilities: opts.capabilities?.split(",").map((s) => s.trim()),
395
+ taskTypeFilter: opts.taskTypes ? opts.taskTypes.split(",").map((s) => s.trim()) : undefined,
396
+ pollInterval: parseInt(opts.pollInterval),
397
+ model: opts.model,
398
+ });
399
+ if (!cfg.apiKey) {
400
+ console.error("Error: API key required.");
401
+ console.error("Run 'cloudcontrol login' or set CLOUDCONTROL_API_KEY.");
402
+ process.exit(1);
403
+ }
404
+ // Set up executor (initializes model router)
405
+ const executor = new TaskExecutor(cfg);
406
+ const availableModels = executor.getAvailableModels();
407
+ console.log(`
408
+ ┌─────────────────────────────────────┐
409
+ │ CloudControl Daemon v${DAEMON_VERSION.padEnd(7)}│
410
+ ├─────────────────────────────────────┤
411
+ │ Worker: ${cfg.workerName.padEnd(27)}│
412
+ │ Type: ${cfg.workerType.padEnd(27)}│
413
+ │ API: ${cfg.apiUrl.padEnd(27)}│
414
+ │ Platform: ${(os.platform() + "/" + os.arch()).padEnd(25)}│
415
+ │ Poll: ${(cfg.pollInterval / 1000 + "s").padEnd(27)}│
416
+ │ Models: ${availableModels.map((m) => m.name).join(", ").padEnd(27)}│
417
+ └─────────────────────────────────────┘
418
+ `);
419
+ // Register worker (with retry)
420
+ console.log("[daemon] Registering worker...");
421
+ let worker;
422
+ try {
423
+ const registerRes = await fetchWithRetry(`${cfg.apiUrl}/api/workers`, {
424
+ method: "POST",
425
+ headers: {
426
+ authorization: `Bearer ${cfg.apiKey}`,
427
+ "content-type": "application/json",
428
+ },
429
+ body: JSON.stringify({
430
+ name: cfg.workerName,
431
+ workerType: cfg.workerType,
432
+ connectionMode: "poll",
433
+ platform: os.platform(),
434
+ capabilities: cfg.capabilities,
435
+ taskTypeFilter: cfg.taskTypeFilter,
436
+ metadata: {
437
+ arch: os.arch(),
438
+ nodeVersion: process.version,
439
+ daemonVersion: DAEMON_VERSION,
440
+ availableModels,
441
+ },
442
+ }),
443
+ }, {
444
+ maxRetries: 5,
445
+ baseDelayMs: 2000,
446
+ onRetry: (attempt, err) => {
447
+ console.log(`[daemon] Registration retry ${attempt}: ${err.message}`);
448
+ },
449
+ });
450
+ ({ worker } = await registerRes.json());
451
+ }
452
+ catch (err) {
453
+ console.error(`[daemon] Failed to register after retries: ${err.message}`);
454
+ process.exit(1);
455
+ }
456
+ console.log(`[daemon] Registered as worker ${worker.id}`);
457
+ executor.setWorkerId(worker.id);
458
+ // Track if currently executing (one task at a time)
459
+ let executing = false;
460
+ async function executeTask(taskId) {
461
+ if (executing) {
462
+ console.log(`[daemon] Already executing, skipping task ${taskId.slice(0, 8)}`);
463
+ return;
464
+ }
465
+ executing = true;
466
+ try {
467
+ sendHeartbeat("busy");
468
+ const task = await executor.claimTask(taskId);
469
+ if (!task)
470
+ return;
471
+ console.log(`[daemon] Claimed: ${task.title}`);
472
+ await executor.executeTask(task);
473
+ }
474
+ catch (err) {
475
+ console.error(`[daemon] Execution error:`, err.message);
476
+ }
477
+ finally {
478
+ executing = false;
479
+ sendHeartbeat("online");
480
+ }
481
+ }
482
+ async function sendHeartbeat(status = "online") {
483
+ try {
484
+ await fetchWithRetry(`${cfg.apiUrl}/api/workers/heartbeat`, {
485
+ method: "POST",
486
+ headers: {
487
+ authorization: `Bearer ${cfg.apiKey}`,
488
+ "content-type": "application/json",
489
+ },
490
+ body: JSON.stringify({ workerId: worker.id, status }),
491
+ }, { maxRetries: 2, baseDelayMs: 1000 });
492
+ }
493
+ catch {
494
+ // non-fatal
495
+ }
496
+ }
497
+ async function pollForTasks() {
498
+ if (executing)
499
+ return;
500
+ try {
501
+ const pending = await executor.pollTasks();
502
+ if (pending.length > 0) {
503
+ console.log(`[daemon] Found ${pending.length} pending task(s)`);
504
+ await executeTask(pending[0].id);
505
+ }
506
+ }
507
+ catch {
508
+ // non-fatal
509
+ }
510
+ }
511
+ const pollTimer = setInterval(pollForTasks, cfg.pollInterval);
512
+ const heartbeatTimer = setInterval(() => sendHeartbeat(), cfg.heartbeatInterval);
513
+ pollForTasks();
514
+ const shutdown = () => {
515
+ console.log("\n[daemon] Shutting down...");
516
+ clearInterval(pollTimer);
517
+ clearInterval(heartbeatTimer);
518
+ process.exit(0);
519
+ };
520
+ process.on("SIGINT", shutdown);
521
+ process.on("SIGTERM", shutdown);
522
+ console.log(`[daemon] Running. Polling every ${cfg.pollInterval / 1000}s. Press Ctrl+C to stop.`);
523
+ });
524
+ program.parse();
525
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1,13 @@
1
+ export interface DaemonConfig {
2
+ apiUrl: string;
3
+ apiKey: string;
4
+ workerName: string;
5
+ workerType: string;
6
+ capabilities: string[];
7
+ taskTypeFilter?: string[];
8
+ pollInterval: number;
9
+ heartbeatInterval: number;
10
+ model?: string;
11
+ profileName?: string;
12
+ }
13
+ export declare function loadConfig(overrides?: Partial<DaemonConfig>): DaemonConfig;