@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/dist/retry.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exponential backoff retry utility for the daemon.
|
|
3
|
+
* Retries failed operations with increasing delays and jitter.
|
|
4
|
+
*/
|
|
5
|
+
const DEFAULT_OPTIONS = {
|
|
6
|
+
maxRetries: 3,
|
|
7
|
+
baseDelayMs: 1000,
|
|
8
|
+
maxDelayMs: 30000,
|
|
9
|
+
};
|
|
10
|
+
export async function withRetry(fn, options = {}) {
|
|
11
|
+
const maxRetries = options.maxRetries ?? DEFAULT_OPTIONS.maxRetries;
|
|
12
|
+
const baseDelayMs = options.baseDelayMs ?? DEFAULT_OPTIONS.baseDelayMs;
|
|
13
|
+
const maxDelayMs = options.maxDelayMs ?? DEFAULT_OPTIONS.maxDelayMs;
|
|
14
|
+
let lastError = null;
|
|
15
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
16
|
+
try {
|
|
17
|
+
return await fn();
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
lastError = err;
|
|
21
|
+
if (attempt === maxRetries)
|
|
22
|
+
break;
|
|
23
|
+
// Exponential backoff with jitter
|
|
24
|
+
const delay = Math.min(baseDelayMs * Math.pow(2, attempt) + Math.random() * 500, maxDelayMs);
|
|
25
|
+
options.onRetry?.(attempt + 1, lastError);
|
|
26
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
throw lastError;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Fetch wrapper with retry. Returns the Response object.
|
|
33
|
+
* Throws on non-OK responses so retry can catch them.
|
|
34
|
+
*/
|
|
35
|
+
export async function fetchWithRetry(url, init, options = {}) {
|
|
36
|
+
return withRetry(async () => {
|
|
37
|
+
const res = await fetch(url, init);
|
|
38
|
+
if (!res.ok) {
|
|
39
|
+
const text = await res.text().catch(() => "");
|
|
40
|
+
throw new Error(`HTTP ${res.status}: ${text.slice(0, 200)}`);
|
|
41
|
+
}
|
|
42
|
+
return res;
|
|
43
|
+
}, options);
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=retry.js.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon Command Sandbox
|
|
3
|
+
*
|
|
4
|
+
* Provides security restrictions for AI task execution:
|
|
5
|
+
* - Command allowlist/denylist
|
|
6
|
+
* - Filesystem path restrictions
|
|
7
|
+
* - Environment variable filtering
|
|
8
|
+
* - Output size limits
|
|
9
|
+
*/
|
|
10
|
+
export interface SandboxConfig {
|
|
11
|
+
allowedCommands: string[];
|
|
12
|
+
blockedCommands: string[];
|
|
13
|
+
allowedPaths: string[];
|
|
14
|
+
blockedPaths: string[];
|
|
15
|
+
maxOutputBytes: number;
|
|
16
|
+
allowedEnvVars: string[];
|
|
17
|
+
}
|
|
18
|
+
export declare function getDefaultSandboxConfig(): SandboxConfig;
|
|
19
|
+
/**
|
|
20
|
+
* Build sandbox instructions to inject into the AI's system prompt.
|
|
21
|
+
*/
|
|
22
|
+
export declare function buildSandboxPrompt(config: SandboxConfig): string;
|
|
23
|
+
/**
|
|
24
|
+
* Filter environment variables to only include allowed ones.
|
|
25
|
+
* Strips API keys, secrets, and sensitive configuration.
|
|
26
|
+
*/
|
|
27
|
+
export declare function filterEnvironment(env: NodeJS.ProcessEnv, allowedVars: string[]): Record<string, string>;
|
|
28
|
+
/**
|
|
29
|
+
* Truncate output to max size, appending a warning if truncated.
|
|
30
|
+
*/
|
|
31
|
+
export declare function truncateOutput(output: string, maxBytes: number): string;
|
|
32
|
+
/**
|
|
33
|
+
* Create an isolated temp directory for task execution.
|
|
34
|
+
* Returns the path. Caller is responsible for cleanup.
|
|
35
|
+
*/
|
|
36
|
+
export declare function createTaskTmpDir(taskId: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Clean up a task's temp directory.
|
|
39
|
+
*/
|
|
40
|
+
export declare function cleanupTaskTmpDir(taskId: string): void;
|
|
41
|
+
/**
|
|
42
|
+
* Build a restricted PATH that only includes standard system directories.
|
|
43
|
+
* Strips any user-specific or development tool paths that could be exploited.
|
|
44
|
+
*/
|
|
45
|
+
export declare function getRestrictedPath(): string;
|
|
46
|
+
/**
|
|
47
|
+
* Get spawn options with OS-level restrictions applied.
|
|
48
|
+
* Uses ulimit wrapper on Unix systems for resource limits.
|
|
49
|
+
*/
|
|
50
|
+
export declare function getSandboxedSpawnOptions(config: SandboxConfig, taskId?: string): {
|
|
51
|
+
env: Record<string, string>;
|
|
52
|
+
cwd?: string;
|
|
53
|
+
};
|
package/dist/sandbox.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon Command Sandbox
|
|
3
|
+
*
|
|
4
|
+
* Provides security restrictions for AI task execution:
|
|
5
|
+
* - Command allowlist/denylist
|
|
6
|
+
* - Filesystem path restrictions
|
|
7
|
+
* - Environment variable filtering
|
|
8
|
+
* - Output size limits
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULT_BLOCKED_COMMANDS = [
|
|
11
|
+
"rm -rf /",
|
|
12
|
+
"rm -rf ~",
|
|
13
|
+
"rm -rf /*",
|
|
14
|
+
"sudo",
|
|
15
|
+
"su ",
|
|
16
|
+
"dd ",
|
|
17
|
+
"mkfs",
|
|
18
|
+
"fdisk",
|
|
19
|
+
"format",
|
|
20
|
+
":(){:|:&};:", // fork bomb
|
|
21
|
+
"chmod 777",
|
|
22
|
+
"chmod -R 777",
|
|
23
|
+
"shutdown",
|
|
24
|
+
"reboot",
|
|
25
|
+
"halt",
|
|
26
|
+
"poweroff",
|
|
27
|
+
"kill -9 1",
|
|
28
|
+
"killall",
|
|
29
|
+
"pkill",
|
|
30
|
+
"> /dev/sda",
|
|
31
|
+
"curl | sh",
|
|
32
|
+
"curl | bash",
|
|
33
|
+
"wget | sh",
|
|
34
|
+
"wget | bash",
|
|
35
|
+
"nc -l", // netcat listener
|
|
36
|
+
"ncat -l",
|
|
37
|
+
"python -m http.server",
|
|
38
|
+
"ssh-keygen",
|
|
39
|
+
"cat /etc/shadow",
|
|
40
|
+
"cat /etc/passwd",
|
|
41
|
+
];
|
|
42
|
+
const DEFAULT_BLOCKED_PATHS = [
|
|
43
|
+
"/etc/shadow",
|
|
44
|
+
"/etc/passwd",
|
|
45
|
+
"/etc/sudoers",
|
|
46
|
+
"~/.ssh",
|
|
47
|
+
"~/.gnupg",
|
|
48
|
+
"~/.aws",
|
|
49
|
+
"~/.config/gcloud",
|
|
50
|
+
"/var/log",
|
|
51
|
+
"/root",
|
|
52
|
+
"/proc",
|
|
53
|
+
"/sys",
|
|
54
|
+
];
|
|
55
|
+
const DEFAULT_ALLOWED_ENV_VARS = [
|
|
56
|
+
"HOME",
|
|
57
|
+
"USER",
|
|
58
|
+
"PATH",
|
|
59
|
+
"SHELL",
|
|
60
|
+
"TERM",
|
|
61
|
+
"LANG",
|
|
62
|
+
"LC_ALL",
|
|
63
|
+
"NODE_ENV",
|
|
64
|
+
"TMPDIR",
|
|
65
|
+
"TMP",
|
|
66
|
+
"TEMP",
|
|
67
|
+
];
|
|
68
|
+
const DEFAULT_MAX_OUTPUT = 1024 * 1024; // 1MB
|
|
69
|
+
export function getDefaultSandboxConfig() {
|
|
70
|
+
return {
|
|
71
|
+
allowedCommands: [],
|
|
72
|
+
blockedCommands: DEFAULT_BLOCKED_COMMANDS,
|
|
73
|
+
allowedPaths: [],
|
|
74
|
+
blockedPaths: DEFAULT_BLOCKED_PATHS,
|
|
75
|
+
maxOutputBytes: DEFAULT_MAX_OUTPUT,
|
|
76
|
+
allowedEnvVars: DEFAULT_ALLOWED_ENV_VARS,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Build sandbox instructions to inject into the AI's system prompt.
|
|
81
|
+
*/
|
|
82
|
+
export function buildSandboxPrompt(config) {
|
|
83
|
+
const lines = [
|
|
84
|
+
"\n## Security Restrictions",
|
|
85
|
+
"You MUST follow these security rules strictly:",
|
|
86
|
+
];
|
|
87
|
+
if (config.blockedCommands.length > 0) {
|
|
88
|
+
lines.push("\n### Blocked Commands (NEVER execute these):");
|
|
89
|
+
for (const cmd of config.blockedCommands) {
|
|
90
|
+
lines.push(`- ${cmd}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (config.allowedCommands.length > 0) {
|
|
94
|
+
lines.push("\n### Allowed Commands (ONLY use these):");
|
|
95
|
+
for (const cmd of config.allowedCommands) {
|
|
96
|
+
lines.push(`- ${cmd}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (config.blockedPaths.length > 0) {
|
|
100
|
+
lines.push("\n### Blocked Paths (NEVER read, write, or access):");
|
|
101
|
+
for (const p of config.blockedPaths) {
|
|
102
|
+
lines.push(`- ${p}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (config.allowedPaths.length > 0) {
|
|
106
|
+
lines.push("\n### Allowed Paths (ONLY access files within these):");
|
|
107
|
+
for (const p of config.allowedPaths) {
|
|
108
|
+
lines.push(`- ${p}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
lines.push("\n### General Rules:");
|
|
112
|
+
lines.push("- Do NOT attempt to access environment variables or secrets directly");
|
|
113
|
+
lines.push("- Do NOT install system packages or modify system configuration");
|
|
114
|
+
lines.push("- Do NOT open network listeners or reverse shells");
|
|
115
|
+
lines.push("- Do NOT access or modify SSH keys, credentials, or auth tokens");
|
|
116
|
+
lines.push("- If a task requires a blocked action, report it with [HUMAN_REQUIRED]");
|
|
117
|
+
return lines.join("\n");
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Filter environment variables to only include allowed ones.
|
|
121
|
+
* Strips API keys, secrets, and sensitive configuration.
|
|
122
|
+
*/
|
|
123
|
+
export function filterEnvironment(env, allowedVars) {
|
|
124
|
+
const filtered = {};
|
|
125
|
+
for (const key of allowedVars) {
|
|
126
|
+
if (env[key]) {
|
|
127
|
+
filtered[key] = env[key];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return filtered;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Truncate output to max size, appending a warning if truncated.
|
|
134
|
+
*/
|
|
135
|
+
export function truncateOutput(output, maxBytes) {
|
|
136
|
+
if (Buffer.byteLength(output, "utf-8") <= maxBytes)
|
|
137
|
+
return output;
|
|
138
|
+
// Find a safe cut point
|
|
139
|
+
const truncated = Buffer.from(output, "utf-8").subarray(0, maxBytes).toString("utf-8");
|
|
140
|
+
return truncated + "\n\n[OUTPUT TRUNCATED — exceeded maximum size]";
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Create an isolated temp directory for task execution.
|
|
144
|
+
* Returns the path. Caller is responsible for cleanup.
|
|
145
|
+
*/
|
|
146
|
+
export function createTaskTmpDir(taskId) {
|
|
147
|
+
const os = require("os");
|
|
148
|
+
const fs = require("fs");
|
|
149
|
+
const path = require("path");
|
|
150
|
+
const dir = path.join(os.tmpdir(), `cloudcontrol-${taskId.slice(0, 8)}`);
|
|
151
|
+
if (!fs.existsSync(dir)) {
|
|
152
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
153
|
+
}
|
|
154
|
+
return dir;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Clean up a task's temp directory.
|
|
158
|
+
*/
|
|
159
|
+
export function cleanupTaskTmpDir(taskId) {
|
|
160
|
+
const os = require("os");
|
|
161
|
+
const fs = require("fs");
|
|
162
|
+
const path = require("path");
|
|
163
|
+
const dir = path.join(os.tmpdir(), `cloudcontrol-${taskId.slice(0, 8)}`);
|
|
164
|
+
try {
|
|
165
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// cleanup failure is non-fatal
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Build a restricted PATH that only includes standard system directories.
|
|
173
|
+
* Strips any user-specific or development tool paths that could be exploited.
|
|
174
|
+
*/
|
|
175
|
+
export function getRestrictedPath() {
|
|
176
|
+
const safeDirs = [
|
|
177
|
+
"/usr/local/bin",
|
|
178
|
+
"/usr/bin",
|
|
179
|
+
"/bin",
|
|
180
|
+
"/usr/sbin",
|
|
181
|
+
"/sbin",
|
|
182
|
+
];
|
|
183
|
+
// Also allow the directory containing the CLI tool itself
|
|
184
|
+
const originalPath = process.env.PATH || "";
|
|
185
|
+
const homeBin = process.env.HOME ? `${process.env.HOME}/.local/bin` : "";
|
|
186
|
+
const dirs = [...safeDirs];
|
|
187
|
+
if (homeBin)
|
|
188
|
+
dirs.push(homeBin);
|
|
189
|
+
// Include node/npm paths so CLI tools can find their dependencies
|
|
190
|
+
for (const dir of originalPath.split(":")) {
|
|
191
|
+
if (dir.includes("node") || dir.includes("npm") || dir.includes("nvm") || dir.includes("fnm")) {
|
|
192
|
+
dirs.push(dir);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return [...new Set(dirs)].join(":");
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Get spawn options with OS-level restrictions applied.
|
|
199
|
+
* Uses ulimit wrapper on Unix systems for resource limits.
|
|
200
|
+
*/
|
|
201
|
+
export function getSandboxedSpawnOptions(config, taskId) {
|
|
202
|
+
const env = filterEnvironment(process.env, config.allowedEnvVars);
|
|
203
|
+
// Restrict PATH
|
|
204
|
+
env.PATH = getRestrictedPath();
|
|
205
|
+
// Isolate tmpdir per task
|
|
206
|
+
if (taskId) {
|
|
207
|
+
const tmpDir = createTaskTmpDir(taskId);
|
|
208
|
+
env.TMPDIR = tmpDir;
|
|
209
|
+
env.TMP = tmpDir;
|
|
210
|
+
env.TEMP = tmpDir;
|
|
211
|
+
}
|
|
212
|
+
// Prevent core dumps
|
|
213
|
+
env.CORE = "0";
|
|
214
|
+
return { env, cwd: taskId ? createTaskTmpDir(taskId) : undefined };
|
|
215
|
+
}
|
|
216
|
+
//# sourceMappingURL=sandbox.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type Platform = "macos" | "linux" | "windows" | "unsupported";
|
|
2
|
+
export declare function detectPlatform(): Platform;
|
|
3
|
+
export declare function install(): {
|
|
4
|
+
success: boolean;
|
|
5
|
+
message: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function uninstall(): {
|
|
8
|
+
success: boolean;
|
|
9
|
+
message: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function serviceStatus(): string;
|
|
12
|
+
export declare function getLogPath(): string;
|
|
13
|
+
export declare function getErrorLogPath(): string;
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import { writeFileSync, unlinkSync, existsSync, mkdirSync } from "fs";
|
|
3
|
+
import { join, dirname } from "path";
|
|
4
|
+
import os from "os";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
export function detectPlatform() {
|
|
8
|
+
switch (os.platform()) {
|
|
9
|
+
case "darwin": return "macos";
|
|
10
|
+
case "linux": return "linux";
|
|
11
|
+
case "win32": return "windows";
|
|
12
|
+
default: return "unsupported";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function resolveCloudcontrolBin() {
|
|
16
|
+
try {
|
|
17
|
+
const cmd = os.platform() === "win32" ? "where cloudcontrol" : "which cloudcontrol";
|
|
18
|
+
const result = execSync(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
19
|
+
if (result)
|
|
20
|
+
return result.split("\n")[0].trim();
|
|
21
|
+
}
|
|
22
|
+
catch { /* not in PATH */ }
|
|
23
|
+
return `${process.execPath} ${join(__dirname, "cli.js")}`;
|
|
24
|
+
}
|
|
25
|
+
// ── macOS LaunchAgent ────────────────────────────────
|
|
26
|
+
const PLIST_LABEL = "com.cloudcontrol.daemon";
|
|
27
|
+
function plistPath() {
|
|
28
|
+
return join(os.homedir(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
|
|
29
|
+
}
|
|
30
|
+
function logDir() {
|
|
31
|
+
return join(os.homedir(), ".cloudcontrol", "logs");
|
|
32
|
+
}
|
|
33
|
+
function generatePlist(binPath) {
|
|
34
|
+
const logs = logDir();
|
|
35
|
+
const parts = binPath.split(" ");
|
|
36
|
+
const executable = parts[0];
|
|
37
|
+
const extraArgs = parts.slice(1);
|
|
38
|
+
const argStrings = [executable, ...extraArgs, "start", "--all"]
|
|
39
|
+
.map((a) => ` <string>${a}</string>`)
|
|
40
|
+
.join("\n");
|
|
41
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
42
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
43
|
+
<plist version="1.0">
|
|
44
|
+
<dict>
|
|
45
|
+
<key>Label</key>
|
|
46
|
+
<string>${PLIST_LABEL}</string>
|
|
47
|
+
<key>ProgramArguments</key>
|
|
48
|
+
<array>
|
|
49
|
+
${argStrings}
|
|
50
|
+
</array>
|
|
51
|
+
<key>RunAtLoad</key>
|
|
52
|
+
<true/>
|
|
53
|
+
<key>KeepAlive</key>
|
|
54
|
+
<true/>
|
|
55
|
+
<key>StandardOutPath</key>
|
|
56
|
+
<string>${logs}/daemon.log</string>
|
|
57
|
+
<key>StandardErrorPath</key>
|
|
58
|
+
<string>${logs}/daemon.err</string>
|
|
59
|
+
<key>EnvironmentVariables</key>
|
|
60
|
+
<dict>
|
|
61
|
+
<key>PATH</key>
|
|
62
|
+
<string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:${process.env.HOME}/.nvm/versions/node/${process.version}/bin</string>
|
|
63
|
+
<key>HOME</key>
|
|
64
|
+
<string>${os.homedir()}</string>
|
|
65
|
+
</dict>
|
|
66
|
+
</dict>
|
|
67
|
+
</plist>`;
|
|
68
|
+
}
|
|
69
|
+
function installMacOS() {
|
|
70
|
+
const bin = resolveCloudcontrolBin();
|
|
71
|
+
const plist = plistPath();
|
|
72
|
+
const logs = logDir();
|
|
73
|
+
if (!existsSync(logs))
|
|
74
|
+
mkdirSync(logs, { recursive: true });
|
|
75
|
+
try {
|
|
76
|
+
execSync(`launchctl unload "${plist}" 2>/dev/null`, { stdio: "pipe" });
|
|
77
|
+
}
|
|
78
|
+
catch { /* not loaded */ }
|
|
79
|
+
writeFileSync(plist, generatePlist(bin), "utf-8");
|
|
80
|
+
try {
|
|
81
|
+
execSync(`launchctl load "${plist}"`, { stdio: "pipe" });
|
|
82
|
+
return { success: true, message: `Installed and started.\n Plist: ${plist}\n Logs: ${logs}/daemon.log\n Stop: cloudcontrol uninstall` };
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
return { success: false, message: `Failed to load LaunchAgent: ${err.message}` };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function uninstallMacOS() {
|
|
89
|
+
const plist = plistPath();
|
|
90
|
+
try {
|
|
91
|
+
execSync(`launchctl unload "${plist}" 2>/dev/null`, { stdio: "pipe" });
|
|
92
|
+
}
|
|
93
|
+
catch { /* not loaded */ }
|
|
94
|
+
try {
|
|
95
|
+
unlinkSync(plist);
|
|
96
|
+
}
|
|
97
|
+
catch { /* not found */ }
|
|
98
|
+
return { success: true, message: "Daemon uninstalled. LaunchAgent removed." };
|
|
99
|
+
}
|
|
100
|
+
function statusMacOS() {
|
|
101
|
+
try {
|
|
102
|
+
const out = execSync(`launchctl list | grep ${PLIST_LABEL}`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
103
|
+
const parts = out.trim().split(/\s+/);
|
|
104
|
+
const pid = parts[0];
|
|
105
|
+
if (pid !== "-")
|
|
106
|
+
return `Running (PID ${pid})`;
|
|
107
|
+
return `Stopped (exit code: ${parts[1]})`;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return "Not installed";
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// ── Linux systemd ────────────────────────────────────
|
|
114
|
+
const SYSTEMD_SERVICE = "cloudcontrol-daemon";
|
|
115
|
+
function systemdPath() {
|
|
116
|
+
return join(os.homedir(), ".config", "systemd", "user", `${SYSTEMD_SERVICE}.service`);
|
|
117
|
+
}
|
|
118
|
+
function generateSystemdUnit(binPath) {
|
|
119
|
+
return `[Unit]
|
|
120
|
+
Description=CloudControl Daemon
|
|
121
|
+
After=network-online.target
|
|
122
|
+
Wants=network-online.target
|
|
123
|
+
|
|
124
|
+
[Service]
|
|
125
|
+
Type=simple
|
|
126
|
+
ExecStart=${binPath} start --all
|
|
127
|
+
Restart=on-failure
|
|
128
|
+
RestartSec=10
|
|
129
|
+
Environment=HOME=${os.homedir()}
|
|
130
|
+
Environment=PATH=/usr/local/bin:/usr/bin:/bin:${dirname(process.execPath)}
|
|
131
|
+
|
|
132
|
+
[Install]
|
|
133
|
+
WantedBy=default.target
|
|
134
|
+
`;
|
|
135
|
+
}
|
|
136
|
+
function installLinux() {
|
|
137
|
+
const bin = resolveCloudcontrolBin();
|
|
138
|
+
const unitPath = systemdPath();
|
|
139
|
+
const unitDir = dirname(unitPath);
|
|
140
|
+
if (!existsSync(unitDir))
|
|
141
|
+
mkdirSync(unitDir, { recursive: true });
|
|
142
|
+
writeFileSync(unitPath, generateSystemdUnit(bin), "utf-8");
|
|
143
|
+
const opts = { encoding: "utf-8", stdio: "pipe" };
|
|
144
|
+
try {
|
|
145
|
+
execSync("systemctl --user daemon-reload", opts);
|
|
146
|
+
execSync(`systemctl --user enable ${SYSTEMD_SERVICE}`, opts);
|
|
147
|
+
execSync(`systemctl --user start ${SYSTEMD_SERVICE}`, opts);
|
|
148
|
+
return { success: true, message: `Installed and started.\n Unit: ${unitPath}\n Logs: journalctl --user -u ${SYSTEMD_SERVICE}\n Stop: cloudcontrol uninstall` };
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
return { success: false, message: `Failed: ${err.message}` };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function uninstallLinux() {
|
|
155
|
+
const unitPath = systemdPath();
|
|
156
|
+
const opts = { encoding: "utf-8", stdio: "pipe" };
|
|
157
|
+
try {
|
|
158
|
+
execSync(`systemctl --user stop ${SYSTEMD_SERVICE}`, opts);
|
|
159
|
+
}
|
|
160
|
+
catch { /* not running */ }
|
|
161
|
+
try {
|
|
162
|
+
execSync(`systemctl --user disable ${SYSTEMD_SERVICE}`, opts);
|
|
163
|
+
}
|
|
164
|
+
catch { /* not enabled */ }
|
|
165
|
+
try {
|
|
166
|
+
unlinkSync(unitPath);
|
|
167
|
+
}
|
|
168
|
+
catch { /* not found */ }
|
|
169
|
+
try {
|
|
170
|
+
execSync("systemctl --user daemon-reload", opts);
|
|
171
|
+
}
|
|
172
|
+
catch { /* ok */ }
|
|
173
|
+
return { success: true, message: "Daemon uninstalled. systemd service removed." };
|
|
174
|
+
}
|
|
175
|
+
function statusLinux() {
|
|
176
|
+
try {
|
|
177
|
+
const out = execSync(`systemctl --user is-active ${SYSTEMD_SERVICE}`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
178
|
+
if (out === "active") {
|
|
179
|
+
const pid = execSync(`systemctl --user show ${SYSTEMD_SERVICE} --property=MainPID --value`, { encoding: "utf-8", stdio: "pipe" }).trim();
|
|
180
|
+
return `Running (PID ${pid})`;
|
|
181
|
+
}
|
|
182
|
+
return `Stopped (${out})`;
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return "Not installed";
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// ── Windows Scheduled Task ───────────────────────────
|
|
189
|
+
const WIN_TASK_NAME = "CloudControlDaemon";
|
|
190
|
+
function installWindows() {
|
|
191
|
+
const bin = resolveCloudcontrolBin();
|
|
192
|
+
const logs = logDir();
|
|
193
|
+
if (!existsSync(logs))
|
|
194
|
+
mkdirSync(logs, { recursive: true });
|
|
195
|
+
try {
|
|
196
|
+
execSync(`schtasks /create /tn "${WIN_TASK_NAME}" /tr "${bin} start --all" /sc ONLOGON /rl HIGHEST /f`, { encoding: "utf-8", stdio: "pipe" });
|
|
197
|
+
execSync(`schtasks /run /tn "${WIN_TASK_NAME}"`, { encoding: "utf-8", stdio: "pipe" });
|
|
198
|
+
return { success: true, message: `Installed and started.\n Task: ${WIN_TASK_NAME}\n Stop: cloudcontrol uninstall` };
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
return { success: false, message: `Failed: ${err.message}` };
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function uninstallWindows() {
|
|
205
|
+
try {
|
|
206
|
+
execSync(`schtasks /end /tn "${WIN_TASK_NAME}"`, { encoding: "utf-8", stdio: "pipe" });
|
|
207
|
+
}
|
|
208
|
+
catch { /* not running */ }
|
|
209
|
+
try {
|
|
210
|
+
execSync(`schtasks /delete /tn "${WIN_TASK_NAME}" /f`, { encoding: "utf-8", stdio: "pipe" });
|
|
211
|
+
return { success: true, message: "Daemon uninstalled. Scheduled task removed." };
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return { success: true, message: "Daemon not installed (no task found)." };
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function statusWindows() {
|
|
218
|
+
try {
|
|
219
|
+
const out = execSync(`schtasks /query /tn "${WIN_TASK_NAME}" /fo CSV /nh`, { encoding: "utf-8", stdio: "pipe" }).trim();
|
|
220
|
+
if (out.includes("Running"))
|
|
221
|
+
return "Running";
|
|
222
|
+
return "Stopped";
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return "Not installed";
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// ── Public API ───────────────────────────────────────
|
|
229
|
+
export function install() {
|
|
230
|
+
const platform = detectPlatform();
|
|
231
|
+
switch (platform) {
|
|
232
|
+
case "macos": return installMacOS();
|
|
233
|
+
case "linux": return installLinux();
|
|
234
|
+
case "windows": return installWindows();
|
|
235
|
+
default: return { success: false, message: `Unsupported platform: ${os.platform()}` };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
export function uninstall() {
|
|
239
|
+
const platform = detectPlatform();
|
|
240
|
+
switch (platform) {
|
|
241
|
+
case "macos": return uninstallMacOS();
|
|
242
|
+
case "linux": return uninstallLinux();
|
|
243
|
+
case "windows": return uninstallWindows();
|
|
244
|
+
default: return { success: false, message: `Unsupported platform: ${os.platform()}` };
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
export function serviceStatus() {
|
|
248
|
+
const platform = detectPlatform();
|
|
249
|
+
switch (platform) {
|
|
250
|
+
case "macos": return statusMacOS();
|
|
251
|
+
case "linux": return statusLinux();
|
|
252
|
+
case "windows": return statusWindows();
|
|
253
|
+
default: return "Unsupported platform";
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
export function getLogPath() {
|
|
257
|
+
return join(logDir(), "daemon.log");
|
|
258
|
+
}
|
|
259
|
+
export function getErrorLogPath() {
|
|
260
|
+
return join(logDir(), "daemon.err");
|
|
261
|
+
}
|
|
262
|
+
//# sourceMappingURL=service-manager.js.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { DaemonConfig } from "./config.js";
|
|
2
|
+
interface Task {
|
|
3
|
+
id: string;
|
|
4
|
+
title: string;
|
|
5
|
+
description?: string | null;
|
|
6
|
+
taskType?: string | null;
|
|
7
|
+
context?: Record<string, unknown> | null;
|
|
8
|
+
credentialsNeeded?: string[];
|
|
9
|
+
processHint?: string | null;
|
|
10
|
+
humanContext?: string | null;
|
|
11
|
+
modelHint?: string | null;
|
|
12
|
+
}
|
|
13
|
+
export declare class TaskExecutor {
|
|
14
|
+
private config;
|
|
15
|
+
private workerId;
|
|
16
|
+
private router;
|
|
17
|
+
private secrets;
|
|
18
|
+
constructor(config: DaemonConfig);
|
|
19
|
+
setWorkerId(id: string): void;
|
|
20
|
+
getAvailableModels(): Array<{
|
|
21
|
+
name: string;
|
|
22
|
+
traits: string[];
|
|
23
|
+
}>;
|
|
24
|
+
private get headers();
|
|
25
|
+
claimTask(taskId: string): Promise<Task | null>;
|
|
26
|
+
executeTask(task: Task): Promise<void>;
|
|
27
|
+
submitResult(taskId: string, result: {
|
|
28
|
+
status: "completed" | "failed" | "human_required";
|
|
29
|
+
result?: Record<string, unknown>;
|
|
30
|
+
dialogue?: Array<{
|
|
31
|
+
role: string;
|
|
32
|
+
content: string;
|
|
33
|
+
timestamp: string;
|
|
34
|
+
}>;
|
|
35
|
+
humanContext?: string;
|
|
36
|
+
metadata?: {
|
|
37
|
+
model?: string;
|
|
38
|
+
tokens_used?: number;
|
|
39
|
+
duration_ms?: number;
|
|
40
|
+
};
|
|
41
|
+
}): Promise<void>;
|
|
42
|
+
private updateTaskStatus;
|
|
43
|
+
private resolveProcess;
|
|
44
|
+
fetchSecret(key: string, taskId: string): Promise<string | null>;
|
|
45
|
+
pollTasks(): Promise<Task[]>;
|
|
46
|
+
}
|
|
47
|
+
export {};
|