@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 +150 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +525 -0
- package/dist/config.d.ts +13 -0
- package/dist/config.js +38 -0
- package/dist/mcp-server.d.ts +26 -0
- package/dist/mcp-server.js +522 -0
- package/dist/model-router.d.ts +40 -0
- package/dist/model-router.js +146 -0
- package/dist/models/claude-code.d.ts +15 -0
- package/dist/models/claude-code.js +140 -0
- package/dist/models/claude.d.ts +34 -0
- package/dist/models/claude.js +121 -0
- package/dist/models/cli-adapter.d.ts +48 -0
- package/dist/models/cli-adapter.js +218 -0
- package/dist/models/ollama.d.ts +25 -0
- package/dist/models/ollama.js +139 -0
- package/dist/multi-profile.d.ts +6 -0
- package/dist/multi-profile.js +137 -0
- package/dist/profile.d.ts +27 -0
- package/dist/profile.js +97 -0
- package/dist/retry.d.ts +17 -0
- package/dist/retry.js +45 -0
- package/dist/sandbox.d.ts +53 -0
- package/dist/sandbox.js +216 -0
- package/dist/service-manager.d.ts +13 -0
- package/dist/service-manager.js +262 -0
- package/dist/task-executor.d.ts +47 -0
- package/dist/task-executor.js +195 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +17 -0
- package/package.json +36 -0
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
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
|
package/dist/config.d.ts
ADDED
|
@@ -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;
|