@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.
@@ -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;
@@ -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
+ }