@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
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { ModelRouter } from "./model-router.js";
|
|
2
|
+
import { fetchWithRetry } from "./retry.js";
|
|
3
|
+
import { cleanupTaskTmpDir } from "./sandbox.js";
|
|
4
|
+
const retryOpts = {
|
|
5
|
+
maxRetries: 3,
|
|
6
|
+
baseDelayMs: 1000,
|
|
7
|
+
onRetry: (attempt, err) => {
|
|
8
|
+
console.log(`[executor] Retry attempt ${attempt}: ${err.message}`);
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
export class TaskExecutor {
|
|
12
|
+
config;
|
|
13
|
+
workerId = null;
|
|
14
|
+
router;
|
|
15
|
+
secrets = new Map();
|
|
16
|
+
constructor(config) {
|
|
17
|
+
this.config = config;
|
|
18
|
+
this.router = new ModelRouter();
|
|
19
|
+
}
|
|
20
|
+
setWorkerId(id) {
|
|
21
|
+
this.workerId = id;
|
|
22
|
+
}
|
|
23
|
+
getAvailableModels() {
|
|
24
|
+
return this.router.listModels();
|
|
25
|
+
}
|
|
26
|
+
get headers() {
|
|
27
|
+
return {
|
|
28
|
+
authorization: `Bearer ${this.config.apiKey}`,
|
|
29
|
+
"content-type": "application/json",
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
async claimTask(taskId) {
|
|
33
|
+
if (!this.workerId)
|
|
34
|
+
throw new Error("Worker not registered");
|
|
35
|
+
try {
|
|
36
|
+
const res = await fetchWithRetry(`${this.config.apiUrl}/api/tasks/${taskId}/claim`, {
|
|
37
|
+
method: "POST",
|
|
38
|
+
headers: this.headers,
|
|
39
|
+
body: JSON.stringify({ workerId: this.workerId }),
|
|
40
|
+
}, retryOpts);
|
|
41
|
+
const { task } = await res.json();
|
|
42
|
+
return task;
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
console.log(`[executor] Failed to claim task ${taskId}: ${err.message}`);
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async executeTask(task) {
|
|
50
|
+
console.log(`[executor] Executing: ${task.title}`);
|
|
51
|
+
// Update status to running
|
|
52
|
+
await this.updateTaskStatus(task.id, "running");
|
|
53
|
+
// Resolve process for this task type (if any)
|
|
54
|
+
if (task.taskType) {
|
|
55
|
+
const processSteps = await this.resolveProcess(task.taskType);
|
|
56
|
+
if (processSteps) {
|
|
57
|
+
// Inject process steps into the task's processHint
|
|
58
|
+
task.processHint = processSteps;
|
|
59
|
+
console.log(`[executor] Process loaded for type "${task.taskType}"`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Fetch any needed secrets
|
|
63
|
+
if (task.credentialsNeeded && task.credentialsNeeded.length > 0) {
|
|
64
|
+
for (const key of task.credentialsNeeded) {
|
|
65
|
+
const value = await this.fetchSecret(key, task.id);
|
|
66
|
+
if (value) {
|
|
67
|
+
this.secrets.set(key, value);
|
|
68
|
+
console.log(`[executor] Secret "${key}" loaded`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Select model based on task's hint
|
|
73
|
+
const { adapter, name: modelName } = this.router.select(task.modelHint);
|
|
74
|
+
console.log(`[executor] Model: ${modelName} (hint: ${task.modelHint || "auto"})`);
|
|
75
|
+
// Pass taskId to adapter for sandbox isolation
|
|
76
|
+
if ("setTaskId" in adapter && typeof adapter.setTaskId === "function") {
|
|
77
|
+
adapter.setTaskId(task.id);
|
|
78
|
+
}
|
|
79
|
+
// Execute with selected model adapter
|
|
80
|
+
let result;
|
|
81
|
+
try {
|
|
82
|
+
result = await adapter.execute(task);
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
// Flush secrets and cleanup sandbox tmpdir
|
|
86
|
+
this.secrets.clear();
|
|
87
|
+
cleanupTaskTmpDir(task.id);
|
|
88
|
+
}
|
|
89
|
+
// Submit result
|
|
90
|
+
if (result.humanRequired) {
|
|
91
|
+
console.log(`[executor] Task requires human input: ${result.humanRequired.reason}`);
|
|
92
|
+
await this.submitResult(task.id, {
|
|
93
|
+
status: "human_required",
|
|
94
|
+
result: result.result,
|
|
95
|
+
dialogue: result.dialogue,
|
|
96
|
+
humanContext: result.humanRequired.reason,
|
|
97
|
+
metadata: result.metadata,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
else if (result.success) {
|
|
101
|
+
console.log(`[executor] Task completed successfully`);
|
|
102
|
+
await this.submitResult(task.id, {
|
|
103
|
+
status: "completed",
|
|
104
|
+
result: result.result,
|
|
105
|
+
dialogue: result.dialogue,
|
|
106
|
+
metadata: result.metadata,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
console.log(`[executor] Task failed`);
|
|
111
|
+
await this.submitResult(task.id, {
|
|
112
|
+
status: "failed",
|
|
113
|
+
result: result.result,
|
|
114
|
+
dialogue: result.dialogue,
|
|
115
|
+
metadata: result.metadata,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async submitResult(taskId, result) {
|
|
120
|
+
if (!this.workerId)
|
|
121
|
+
throw new Error("Worker not registered");
|
|
122
|
+
await fetchWithRetry(`${this.config.apiUrl}/api/tasks/${taskId}/submit`, {
|
|
123
|
+
method: "POST",
|
|
124
|
+
headers: this.headers,
|
|
125
|
+
body: JSON.stringify({ workerId: this.workerId, ...result }),
|
|
126
|
+
}, retryOpts);
|
|
127
|
+
console.log(`[executor] Task ${taskId} submitted as ${result.status}`);
|
|
128
|
+
}
|
|
129
|
+
async updateTaskStatus(taskId, status) {
|
|
130
|
+
try {
|
|
131
|
+
await fetchWithRetry(`${this.config.apiUrl}/api/tasks/${taskId}`, {
|
|
132
|
+
method: "PATCH",
|
|
133
|
+
headers: this.headers,
|
|
134
|
+
body: JSON.stringify({ status }),
|
|
135
|
+
}, { maxRetries: 2, baseDelayMs: 500 });
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// Status update failure is non-fatal — task will still be processed
|
|
139
|
+
console.log(`[executor] Warning: failed to update status to ${status}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async resolveProcess(taskType) {
|
|
143
|
+
try {
|
|
144
|
+
const res = await fetchWithRetry(`${this.config.apiUrl}/api/processes/resolve?taskType=${encodeURIComponent(taskType)}`, { headers: this.headers }, { maxRetries: 2, baseDelayMs: 1000 });
|
|
145
|
+
const { parsed } = await res.json();
|
|
146
|
+
if (!parsed?.steps?.length)
|
|
147
|
+
return null;
|
|
148
|
+
// Format steps as instructions for the AI
|
|
149
|
+
const lines = [`Follow this process (${parsed.name} v${parsed.version}):\n`];
|
|
150
|
+
for (let i = 0; i < parsed.steps.length; i++) {
|
|
151
|
+
const step = parsed.steps[i];
|
|
152
|
+
let desc = `Step ${i + 1} [${step.type}]: ${step.id}`;
|
|
153
|
+
if (step.prompt)
|
|
154
|
+
desc += ` — ${step.prompt}`;
|
|
155
|
+
if (step.action)
|
|
156
|
+
desc += ` — action: ${step.action}`;
|
|
157
|
+
if (step.target)
|
|
158
|
+
desc += ` (target: ${step.target})`;
|
|
159
|
+
if (step.gate === "approval_required")
|
|
160
|
+
desc += ` [REQUIRES HUMAN APPROVAL]`;
|
|
161
|
+
lines.push(desc);
|
|
162
|
+
}
|
|
163
|
+
if (parsed.credentialsNeeded?.length > 0) {
|
|
164
|
+
lines.push(`\nCredentials available: ${parsed.credentialsNeeded.join(", ")}`);
|
|
165
|
+
}
|
|
166
|
+
return lines.join("\n");
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
// Process resolution failure is non-fatal
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async fetchSecret(key, taskId) {
|
|
174
|
+
try {
|
|
175
|
+
const res = await fetchWithRetry(`${this.config.apiUrl}/api/secrets/${encodeURIComponent(key)}?task_id=${taskId}`, { headers: this.headers }, retryOpts);
|
|
176
|
+
const { value } = await res.json();
|
|
177
|
+
return value;
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
console.log(`[executor] Failed to fetch secret "${key}": ${err.message}`);
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async pollTasks() {
|
|
185
|
+
try {
|
|
186
|
+
const res = await fetchWithRetry(`${this.config.apiUrl}/api/tasks?status=pending&limit=10&workerId=${this.workerId}`, { headers: this.headers }, { maxRetries: 2, baseDelayMs: 2000 });
|
|
187
|
+
const { tasks } = await res.json();
|
|
188
|
+
return tasks;
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
//# sourceMappingURL=task-executor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const DAEMON_VERSION: string;
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import { dirname, join } from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
function readVersion() {
|
|
6
|
+
// Works from both src/ (dev with tsx) and dist/ (compiled)
|
|
7
|
+
for (const rel of ["../package.json", "../../package.json"]) {
|
|
8
|
+
try {
|
|
9
|
+
const raw = readFileSync(join(__dirname, rel), "utf-8");
|
|
10
|
+
return JSON.parse(raw).version;
|
|
11
|
+
}
|
|
12
|
+
catch { /* try next */ }
|
|
13
|
+
}
|
|
14
|
+
return "0.0.0";
|
|
15
|
+
}
|
|
16
|
+
export const DAEMON_VERSION = readVersion();
|
|
17
|
+
//# sourceMappingURL=version.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@epic-cloudcontrol/daemon",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "CloudControl local daemon — executes AI agent tasks on worker machines",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/cli.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"cloudcontrol": "dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/**/*.js",
|
|
12
|
+
"dist/**/*.d.ts"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"dev": "tsx src/cli.ts",
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"prepublishOnly": "npm run build",
|
|
18
|
+
"start": "node dist/cli.js start"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=20.0.0"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@anthropic-ai/sdk": "^0.82.0",
|
|
25
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
26
|
+
"commander": "^13.0.0",
|
|
27
|
+
"dotenv": "^17.4.0",
|
|
28
|
+
"yaml": "^2.8.3",
|
|
29
|
+
"zod": "^4.3.6"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^22.0.0",
|
|
33
|
+
"tsx": "^4.19.0",
|
|
34
|
+
"typescript": "^5.7.0"
|
|
35
|
+
}
|
|
36
|
+
}
|