@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,218 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import { buildSandboxPrompt, getDefaultSandboxConfig, getSandboxedSpawnOptions, truncateOutput, } from "../sandbox.js";
|
|
3
|
+
/**
|
|
4
|
+
* Predefined CLI configurations for known tools.
|
|
5
|
+
* Add new CLIs here — no other code changes needed.
|
|
6
|
+
*/
|
|
7
|
+
export const KNOWN_CLIS = {
|
|
8
|
+
"claude-code": {
|
|
9
|
+
command: "claude",
|
|
10
|
+
args: ["-p", "{prompt}"],
|
|
11
|
+
},
|
|
12
|
+
gemini: {
|
|
13
|
+
command: "gemini",
|
|
14
|
+
args: ["-p", "{prompt}"],
|
|
15
|
+
},
|
|
16
|
+
codex: {
|
|
17
|
+
command: "codex",
|
|
18
|
+
args: ["-p", "{prompt}"],
|
|
19
|
+
},
|
|
20
|
+
aider: {
|
|
21
|
+
command: "aider",
|
|
22
|
+
args: ["--message", "{prompt}", "--yes-always", "--no-git"],
|
|
23
|
+
},
|
|
24
|
+
goose: {
|
|
25
|
+
command: "goose",
|
|
26
|
+
args: ["run", "--text", "{prompt}"],
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Generic CLI adapter — works with any AI CLI tool that accepts a prompt
|
|
31
|
+
* and writes output to stdout. Configure via CliModelConfig or use a
|
|
32
|
+
* predefined config from KNOWN_CLIS.
|
|
33
|
+
*/
|
|
34
|
+
export class CliAdapter {
|
|
35
|
+
config;
|
|
36
|
+
sandboxConfig;
|
|
37
|
+
constructor(config, sandboxConfig) {
|
|
38
|
+
this.config = {
|
|
39
|
+
...config,
|
|
40
|
+
env: config.env || {},
|
|
41
|
+
timeoutMs: config.timeoutMs || 5 * 60 * 1000,
|
|
42
|
+
};
|
|
43
|
+
this.sandboxConfig = sandboxConfig || getDefaultSandboxConfig();
|
|
44
|
+
}
|
|
45
|
+
async execute(task) {
|
|
46
|
+
const dialogue = [];
|
|
47
|
+
const startTime = Date.now();
|
|
48
|
+
// Build prompt with sandbox restrictions
|
|
49
|
+
const sandboxRules = buildSandboxPrompt(this.sandboxConfig);
|
|
50
|
+
let prompt = `## Task: ${task.title}\n`;
|
|
51
|
+
if (task.description)
|
|
52
|
+
prompt += `\n${task.description}\n`;
|
|
53
|
+
if (task.taskType)
|
|
54
|
+
prompt += `\nTask type: ${task.taskType}\n`;
|
|
55
|
+
if (task.processHint)
|
|
56
|
+
prompt += `\nProcess hint: ${task.processHint}\n`;
|
|
57
|
+
if (task.context) {
|
|
58
|
+
prompt += `\nContext:\n\`\`\`json\n${JSON.stringify(task.context, null, 2)}\n\`\`\`\n`;
|
|
59
|
+
}
|
|
60
|
+
if (task.humanContext) {
|
|
61
|
+
prompt += `\nHuman feedback from previous attempt:\n${task.humanContext}\n`;
|
|
62
|
+
}
|
|
63
|
+
prompt += `\nWhen you encounter something you cannot complete (CAPTCHA, phone verification, account creation, ambiguous decision), say: [HUMAN_REQUIRED]: <reason>\n`;
|
|
64
|
+
prompt += `\nWhen done, end with: [RESULT]: <brief summary>\n`;
|
|
65
|
+
prompt += sandboxRules;
|
|
66
|
+
dialogue.push({
|
|
67
|
+
role: "user",
|
|
68
|
+
content: prompt,
|
|
69
|
+
timestamp: new Date().toISOString(),
|
|
70
|
+
});
|
|
71
|
+
try {
|
|
72
|
+
const output = await this.runCli(prompt);
|
|
73
|
+
dialogue.push({
|
|
74
|
+
role: "assistant",
|
|
75
|
+
content: output,
|
|
76
|
+
timestamp: new Date().toISOString(),
|
|
77
|
+
});
|
|
78
|
+
// Check for human required
|
|
79
|
+
const humanMatch = output.match(/\[HUMAN_REQUIRED\]:\s*(.*)/s);
|
|
80
|
+
if (humanMatch) {
|
|
81
|
+
return {
|
|
82
|
+
success: false,
|
|
83
|
+
dialogue,
|
|
84
|
+
result: { raw_response: output },
|
|
85
|
+
metadata: {
|
|
86
|
+
model: this.config.name,
|
|
87
|
+
tokens_used: 0,
|
|
88
|
+
duration_ms: Date.now() - startTime,
|
|
89
|
+
},
|
|
90
|
+
humanRequired: {
|
|
91
|
+
reason: humanMatch[1].trim(),
|
|
92
|
+
context: output,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const resultMatch = output.match(/\[RESULT\]:\s*(.*)/s);
|
|
97
|
+
const resultSummary = resultMatch
|
|
98
|
+
? resultMatch[1].trim()
|
|
99
|
+
: output.slice(0, 500);
|
|
100
|
+
return {
|
|
101
|
+
success: true,
|
|
102
|
+
dialogue,
|
|
103
|
+
result: { summary: resultSummary, raw_response: output },
|
|
104
|
+
metadata: {
|
|
105
|
+
model: this.config.name,
|
|
106
|
+
tokens_used: 0,
|
|
107
|
+
duration_ms: Date.now() - startTime,
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
const errMsg = error instanceof Error ? error.message : "Unknown error";
|
|
113
|
+
dialogue.push({
|
|
114
|
+
role: "system",
|
|
115
|
+
content: `Execution error: ${errMsg}`,
|
|
116
|
+
timestamp: new Date().toISOString(),
|
|
117
|
+
});
|
|
118
|
+
return {
|
|
119
|
+
success: false,
|
|
120
|
+
dialogue,
|
|
121
|
+
result: { error: errMsg },
|
|
122
|
+
metadata: {
|
|
123
|
+
model: this.config.name,
|
|
124
|
+
tokens_used: 0,
|
|
125
|
+
duration_ms: Date.now() - startTime,
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
taskId;
|
|
131
|
+
setTaskId(taskId) {
|
|
132
|
+
this.taskId = taskId;
|
|
133
|
+
}
|
|
134
|
+
runCli(prompt) {
|
|
135
|
+
return new Promise((resolve, reject) => {
|
|
136
|
+
// Get sandboxed environment (restricted PATH, isolated tmpdir)
|
|
137
|
+
const sandboxOpts = getSandboxedSpawnOptions(this.sandboxConfig, this.taskId);
|
|
138
|
+
const env = { ...sandboxOpts.env, ...this.config.env };
|
|
139
|
+
// Substitute {prompt} placeholder in args
|
|
140
|
+
const args = this.config.args.map((arg) => arg === "{prompt}" ? prompt : arg);
|
|
141
|
+
const proc = spawn(this.config.command, args, {
|
|
142
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
143
|
+
env,
|
|
144
|
+
cwd: sandboxOpts.cwd,
|
|
145
|
+
});
|
|
146
|
+
let stdout = "";
|
|
147
|
+
let stderr = "";
|
|
148
|
+
const maxOutput = this.sandboxConfig.maxOutputBytes;
|
|
149
|
+
proc.stdout.on("data", (data) => {
|
|
150
|
+
stdout += data.toString();
|
|
151
|
+
if (Buffer.byteLength(stdout, "utf-8") > maxOutput * 1.1) {
|
|
152
|
+
proc.kill();
|
|
153
|
+
reject(new Error("Output exceeded maximum size limit"));
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
proc.stderr.on("data", (data) => {
|
|
157
|
+
stderr += data.toString();
|
|
158
|
+
});
|
|
159
|
+
proc.on("close", (code) => {
|
|
160
|
+
const truncatedOutput = truncateOutput(stdout.trim(), maxOutput);
|
|
161
|
+
if (code === 0) {
|
|
162
|
+
resolve(truncatedOutput);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
reject(new Error(`${this.config.command} exited with code ${code}: ${stderr.slice(0, 500)}`));
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
proc.on("error", (err) => {
|
|
169
|
+
if (err.code === "ENOENT") {
|
|
170
|
+
reject(new Error(`CLI "${this.config.command}" not found in PATH. Install it or use a different model.`));
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
reject(err);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
setTimeout(() => {
|
|
177
|
+
proc.kill();
|
|
178
|
+
reject(new Error(`${this.config.command} execution timed out (${Math.round(this.config.timeoutMs / 1000)}s)`));
|
|
179
|
+
}, this.config.timeoutMs);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Parse CLI model definitions from an env var string.
|
|
185
|
+
* Format: "name:command:arg1,arg2;name2:command2:arg1,arg2"
|
|
186
|
+
* Example: "gemini:gemini:-p;aider:aider:--message,--yes-always,--no-git"
|
|
187
|
+
*
|
|
188
|
+
* If only a name is given (e.g., "gemini"), looks it up in KNOWN_CLIS.
|
|
189
|
+
*/
|
|
190
|
+
export function parseCliModels(envValue) {
|
|
191
|
+
const configs = [];
|
|
192
|
+
for (const entry of envValue.split(";")) {
|
|
193
|
+
const trimmed = entry.trim();
|
|
194
|
+
if (!trimmed)
|
|
195
|
+
continue;
|
|
196
|
+
const parts = trimmed.split(":");
|
|
197
|
+
const name = parts[0].trim();
|
|
198
|
+
// Check if it's a known CLI (just the name)
|
|
199
|
+
if (parts.length === 1 && KNOWN_CLIS[name]) {
|
|
200
|
+
configs.push({ name, ...KNOWN_CLIS[name] });
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
// Custom definition: name:command:args
|
|
204
|
+
if (parts.length >= 2) {
|
|
205
|
+
const command = parts[1].trim();
|
|
206
|
+
const args = parts.length >= 3
|
|
207
|
+
? parts[2].split(",").map((a) => a.trim())
|
|
208
|
+
: ["-p", "{prompt}"];
|
|
209
|
+
// Ensure {prompt} is in args somewhere
|
|
210
|
+
if (!args.includes("{prompt}")) {
|
|
211
|
+
args.push("{prompt}");
|
|
212
|
+
}
|
|
213
|
+
configs.push({ name, command, args });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return configs;
|
|
217
|
+
}
|
|
218
|
+
//# sourceMappingURL=cli-adapter.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ollama model adapter for local LLM execution.
|
|
3
|
+
* Calls a locally-running Ollama server via its HTTP API.
|
|
4
|
+
*
|
|
5
|
+
* Data never leaves the machine — ideal for sensitive tasks.
|
|
6
|
+
*
|
|
7
|
+
* Requires: Ollama running locally (default: http://localhost:11434)
|
|
8
|
+
* Install: https://ollama.ai
|
|
9
|
+
*/
|
|
10
|
+
import type { ExecutionResult } from "./claude.js";
|
|
11
|
+
import { type SandboxConfig } from "../sandbox.js";
|
|
12
|
+
export declare class OllamaAdapter {
|
|
13
|
+
private baseUrl;
|
|
14
|
+
private model;
|
|
15
|
+
private sandboxConfig;
|
|
16
|
+
constructor(model?: string, baseUrl?: string, sandboxConfig?: SandboxConfig);
|
|
17
|
+
execute(task: {
|
|
18
|
+
title: string;
|
|
19
|
+
description?: string | null;
|
|
20
|
+
taskType?: string | null;
|
|
21
|
+
context?: Record<string, unknown> | null;
|
|
22
|
+
processHint?: string | null;
|
|
23
|
+
humanContext?: string | null;
|
|
24
|
+
}): Promise<ExecutionResult>;
|
|
25
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ollama model adapter for local LLM execution.
|
|
3
|
+
* Calls a locally-running Ollama server via its HTTP API.
|
|
4
|
+
*
|
|
5
|
+
* Data never leaves the machine — ideal for sensitive tasks.
|
|
6
|
+
*
|
|
7
|
+
* Requires: Ollama running locally (default: http://localhost:11434)
|
|
8
|
+
* Install: https://ollama.ai
|
|
9
|
+
*/
|
|
10
|
+
import { buildSandboxPrompt, getDefaultSandboxConfig } from "../sandbox.js";
|
|
11
|
+
export class OllamaAdapter {
|
|
12
|
+
baseUrl;
|
|
13
|
+
model;
|
|
14
|
+
sandboxConfig;
|
|
15
|
+
constructor(model = "llama3", baseUrl = "http://localhost:11434", sandboxConfig) {
|
|
16
|
+
this.model = model;
|
|
17
|
+
this.baseUrl = baseUrl;
|
|
18
|
+
this.sandboxConfig = sandboxConfig || getDefaultSandboxConfig();
|
|
19
|
+
}
|
|
20
|
+
async execute(task) {
|
|
21
|
+
const dialogue = [];
|
|
22
|
+
const startTime = Date.now();
|
|
23
|
+
let totalTokens = 0;
|
|
24
|
+
// Build system prompt with sandbox restrictions
|
|
25
|
+
const sandboxRules = buildSandboxPrompt(this.sandboxConfig);
|
|
26
|
+
const systemPrompt = `You are a CloudControl task executor. You complete tasks efficiently and report results clearly.
|
|
27
|
+
|
|
28
|
+
When you encounter something you cannot complete (CAPTCHA, phone verification, account creation, ambiguous decision), respond with:
|
|
29
|
+
[HUMAN_REQUIRED]: <reason>
|
|
30
|
+
|
|
31
|
+
Always end your final response with:
|
|
32
|
+
[RESULT]: <brief summary of what was accomplished>
|
|
33
|
+
${sandboxRules}`;
|
|
34
|
+
// Build user message
|
|
35
|
+
let userMessage = `## Task: ${task.title}\n`;
|
|
36
|
+
if (task.description)
|
|
37
|
+
userMessage += `\n${task.description}\n`;
|
|
38
|
+
if (task.taskType)
|
|
39
|
+
userMessage += `\nTask type: ${task.taskType}\n`;
|
|
40
|
+
if (task.processHint)
|
|
41
|
+
userMessage += `\nProcess hint: ${task.processHint}\n`;
|
|
42
|
+
if (task.context) {
|
|
43
|
+
userMessage += `\nContext:\n\`\`\`json\n${JSON.stringify(task.context, null, 2)}\n\`\`\`\n`;
|
|
44
|
+
}
|
|
45
|
+
if (task.humanContext) {
|
|
46
|
+
userMessage += `\nHuman feedback from previous attempt:\n${task.humanContext}\n`;
|
|
47
|
+
}
|
|
48
|
+
dialogue.push({
|
|
49
|
+
role: "user",
|
|
50
|
+
content: userMessage,
|
|
51
|
+
timestamp: new Date().toISOString(),
|
|
52
|
+
});
|
|
53
|
+
try {
|
|
54
|
+
// Call Ollama chat API
|
|
55
|
+
const response = await fetch(`${this.baseUrl}/api/chat`, {
|
|
56
|
+
method: "POST",
|
|
57
|
+
headers: { "content-type": "application/json" },
|
|
58
|
+
body: JSON.stringify({
|
|
59
|
+
model: this.model,
|
|
60
|
+
messages: [
|
|
61
|
+
{ role: "system", content: systemPrompt },
|
|
62
|
+
{ role: "user", content: userMessage },
|
|
63
|
+
],
|
|
64
|
+
stream: false,
|
|
65
|
+
options: {
|
|
66
|
+
num_predict: 4096,
|
|
67
|
+
},
|
|
68
|
+
}),
|
|
69
|
+
});
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
const text = await response.text();
|
|
72
|
+
throw new Error(`Ollama API error ${response.status}: ${text.slice(0, 200)}`);
|
|
73
|
+
}
|
|
74
|
+
const data = await response.json();
|
|
75
|
+
const assistantContent = data.message?.content || "";
|
|
76
|
+
totalTokens = (data.prompt_eval_count || 0) + (data.eval_count || 0);
|
|
77
|
+
dialogue.push({
|
|
78
|
+
role: "assistant",
|
|
79
|
+
content: assistantContent,
|
|
80
|
+
timestamp: new Date().toISOString(),
|
|
81
|
+
});
|
|
82
|
+
// Check for human required
|
|
83
|
+
const humanMatch = assistantContent.match(/\[HUMAN_REQUIRED\]:\s*(.*)/s);
|
|
84
|
+
if (humanMatch) {
|
|
85
|
+
return {
|
|
86
|
+
success: false,
|
|
87
|
+
dialogue,
|
|
88
|
+
result: { raw_response: assistantContent },
|
|
89
|
+
metadata: {
|
|
90
|
+
model: `ollama/${this.model}`,
|
|
91
|
+
tokens_used: totalTokens,
|
|
92
|
+
duration_ms: Date.now() - startTime,
|
|
93
|
+
},
|
|
94
|
+
humanRequired: {
|
|
95
|
+
reason: humanMatch[1].trim(),
|
|
96
|
+
context: assistantContent,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
// Extract result
|
|
101
|
+
const resultMatch = assistantContent.match(/\[RESULT\]:\s*(.*)/s);
|
|
102
|
+
const resultSummary = resultMatch
|
|
103
|
+
? resultMatch[1].trim()
|
|
104
|
+
: assistantContent.slice(0, 500);
|
|
105
|
+
return {
|
|
106
|
+
success: true,
|
|
107
|
+
dialogue,
|
|
108
|
+
result: {
|
|
109
|
+
summary: resultSummary,
|
|
110
|
+
raw_response: assistantContent,
|
|
111
|
+
},
|
|
112
|
+
metadata: {
|
|
113
|
+
model: `ollama/${this.model}`,
|
|
114
|
+
tokens_used: totalTokens,
|
|
115
|
+
duration_ms: Date.now() - startTime,
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
const errMsg = error instanceof Error ? error.message : "Unknown error";
|
|
121
|
+
dialogue.push({
|
|
122
|
+
role: "system",
|
|
123
|
+
content: `Execution error: ${errMsg}`,
|
|
124
|
+
timestamp: new Date().toISOString(),
|
|
125
|
+
});
|
|
126
|
+
return {
|
|
127
|
+
success: false,
|
|
128
|
+
dialogue,
|
|
129
|
+
result: { error: errMsg },
|
|
130
|
+
metadata: {
|
|
131
|
+
model: `ollama/${this.model}`,
|
|
132
|
+
tokens_used: totalTokens,
|
|
133
|
+
duration_ms: Date.now() - startTime,
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=ollama.js.map
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import os from "os";
|
|
2
|
+
import { loadConfig } from "./config.js";
|
|
3
|
+
import { listProfiles } from "./profile.js";
|
|
4
|
+
import { TaskExecutor } from "./task-executor.js";
|
|
5
|
+
import { fetchWithRetry } from "./retry.js";
|
|
6
|
+
import { DAEMON_VERSION } from "./version.js";
|
|
7
|
+
export async function startAllProfiles(overrides) {
|
|
8
|
+
const profiles = listProfiles();
|
|
9
|
+
if (profiles.length === 0) {
|
|
10
|
+
console.error("No profiles found. Run 'cloudcontrol login' first.");
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
console.log(`\n┌─────────────────────────────────────┐`);
|
|
14
|
+
console.log(`│ CloudControl Daemon v${DAEMON_VERSION.padEnd(7)}│`);
|
|
15
|
+
console.log(`├─────────────────────────────────────┤`);
|
|
16
|
+
console.log(`│ Mode: multi-profile (${String(profiles.length)} co.)${" ".repeat(Math.max(0, 7 - String(profiles.length).length))}│`);
|
|
17
|
+
console.log(`│ Platform: ${(os.platform() + "/" + os.arch()).padEnd(25)}│`);
|
|
18
|
+
console.log(`└─────────────────────────────────────┘\n`);
|
|
19
|
+
const workers = [];
|
|
20
|
+
for (const { name, profile } of profiles) {
|
|
21
|
+
const config = loadConfig({
|
|
22
|
+
profileName: name,
|
|
23
|
+
workerType: overrides.workerType,
|
|
24
|
+
capabilities: overrides.capabilities,
|
|
25
|
+
pollInterval: overrides.pollInterval,
|
|
26
|
+
model: overrides.model,
|
|
27
|
+
});
|
|
28
|
+
if (!config.apiKey) {
|
|
29
|
+
console.log(` [${name}] Skipping — no API key`);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const executor = new TaskExecutor(config);
|
|
33
|
+
const pw = {
|
|
34
|
+
profileName: name,
|
|
35
|
+
teamName: profile.teamName || name,
|
|
36
|
+
config,
|
|
37
|
+
executor,
|
|
38
|
+
workerId: null,
|
|
39
|
+
pollTimer: null,
|
|
40
|
+
heartbeatTimer: null,
|
|
41
|
+
executing: false,
|
|
42
|
+
};
|
|
43
|
+
// Register worker
|
|
44
|
+
try {
|
|
45
|
+
const res = await fetchWithRetry(`${config.apiUrl}/api/workers`, {
|
|
46
|
+
method: "POST",
|
|
47
|
+
headers: {
|
|
48
|
+
authorization: `Bearer ${config.apiKey}`,
|
|
49
|
+
"content-type": "application/json",
|
|
50
|
+
},
|
|
51
|
+
body: JSON.stringify({
|
|
52
|
+
name: config.workerName,
|
|
53
|
+
workerType: config.workerType,
|
|
54
|
+
connectionMode: "poll",
|
|
55
|
+
platform: os.platform(),
|
|
56
|
+
capabilities: config.capabilities,
|
|
57
|
+
taskTypeFilter: config.taskTypeFilter,
|
|
58
|
+
metadata: {
|
|
59
|
+
arch: os.arch(),
|
|
60
|
+
nodeVersion: process.version,
|
|
61
|
+
daemonVersion: DAEMON_VERSION,
|
|
62
|
+
availableModels: executor.getAvailableModels(),
|
|
63
|
+
multiProfile: true,
|
|
64
|
+
},
|
|
65
|
+
}),
|
|
66
|
+
}, { maxRetries: 3, baseDelayMs: 2000 });
|
|
67
|
+
const data = await res.json();
|
|
68
|
+
pw.workerId = data.worker.id;
|
|
69
|
+
executor.setWorkerId(data.worker.id);
|
|
70
|
+
console.log(` [${pw.teamName}] Registered as ${pw.workerId.slice(0, 8)}...`);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
console.error(` [${pw.teamName}] Registration failed: ${err.message}`);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
// Set up polling
|
|
77
|
+
const poll = async () => {
|
|
78
|
+
if (pw.executing)
|
|
79
|
+
return;
|
|
80
|
+
try {
|
|
81
|
+
const tasks = await executor.pollTasks();
|
|
82
|
+
if (tasks.length > 0) {
|
|
83
|
+
pw.executing = true;
|
|
84
|
+
try {
|
|
85
|
+
sendHeartbeat(pw, "busy");
|
|
86
|
+
const task = await executor.claimTask(tasks[0].id);
|
|
87
|
+
if (task) {
|
|
88
|
+
console.log(` [${pw.teamName}] Claimed: ${task.title}`);
|
|
89
|
+
await executor.executeTask(task);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
console.error(` [${pw.teamName}] Execution error: ${err.message}`);
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
pw.executing = false;
|
|
97
|
+
sendHeartbeat(pw, "online");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch { /* non-fatal */ }
|
|
102
|
+
};
|
|
103
|
+
pw.pollTimer = setInterval(poll, config.pollInterval);
|
|
104
|
+
pw.heartbeatTimer = setInterval(() => sendHeartbeat(pw, "online"), config.heartbeatInterval);
|
|
105
|
+
poll();
|
|
106
|
+
workers.push(pw);
|
|
107
|
+
}
|
|
108
|
+
if (workers.length === 0) {
|
|
109
|
+
console.error("\nNo workers started. Check your profiles.");
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
console.log(`\nRunning ${workers.length} worker(s). Press Ctrl+C to stop.\n`);
|
|
113
|
+
return () => {
|
|
114
|
+
for (const pw of workers) {
|
|
115
|
+
if (pw.pollTimer)
|
|
116
|
+
clearInterval(pw.pollTimer);
|
|
117
|
+
if (pw.heartbeatTimer)
|
|
118
|
+
clearInterval(pw.heartbeatTimer);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
async function sendHeartbeat(pw, status) {
|
|
123
|
+
if (!pw.workerId)
|
|
124
|
+
return;
|
|
125
|
+
try {
|
|
126
|
+
await fetchWithRetry(`${pw.config.apiUrl}/api/workers/heartbeat`, {
|
|
127
|
+
method: "POST",
|
|
128
|
+
headers: {
|
|
129
|
+
authorization: `Bearer ${pw.config.apiKey}`,
|
|
130
|
+
"content-type": "application/json",
|
|
131
|
+
},
|
|
132
|
+
body: JSON.stringify({ workerId: pw.workerId, status }),
|
|
133
|
+
}, { maxRetries: 1, baseDelayMs: 1000 });
|
|
134
|
+
}
|
|
135
|
+
catch { /* non-fatal */ }
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=multi-profile.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface Profile {
|
|
2
|
+
apiUrl: string;
|
|
3
|
+
apiKey: string;
|
|
4
|
+
workerName?: string;
|
|
5
|
+
teamName?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function getConfigDir(): string;
|
|
8
|
+
/**
|
|
9
|
+
* Load a named profile. Falls back to "default" if name not specified.
|
|
10
|
+
*/
|
|
11
|
+
export declare function loadProfile(name?: string): Profile | null;
|
|
12
|
+
/**
|
|
13
|
+
* Save a named profile.
|
|
14
|
+
*/
|
|
15
|
+
export declare function saveProfile(profile: Profile, name?: string): void;
|
|
16
|
+
/**
|
|
17
|
+
* List all saved profiles.
|
|
18
|
+
*/
|
|
19
|
+
export declare function listProfiles(): Array<{
|
|
20
|
+
name: string;
|
|
21
|
+
profile: Profile;
|
|
22
|
+
}>;
|
|
23
|
+
/**
|
|
24
|
+
* Delete a named profile.
|
|
25
|
+
*/
|
|
26
|
+
export declare function deleteProfile(name: string): boolean;
|
|
27
|
+
export declare function profileExists(name?: string): boolean;
|
package/dist/profile.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
const CONFIG_DIR = path.join(os.homedir(), ".cloudcontrol");
|
|
5
|
+
const PROFILES_DIR = path.join(CONFIG_DIR, "profiles");
|
|
6
|
+
const DEFAULT_CONFIG = path.join(CONFIG_DIR, "config.json");
|
|
7
|
+
export function getConfigDir() {
|
|
8
|
+
return CONFIG_DIR;
|
|
9
|
+
}
|
|
10
|
+
function getProfilePath(name) {
|
|
11
|
+
if (name === "default")
|
|
12
|
+
return DEFAULT_CONFIG;
|
|
13
|
+
return path.join(PROFILES_DIR, `${name}.json`);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Load a named profile. Falls back to "default" if name not specified.
|
|
17
|
+
*/
|
|
18
|
+
export function loadProfile(name) {
|
|
19
|
+
const profileName = name || "default";
|
|
20
|
+
const filePath = getProfilePath(profileName);
|
|
21
|
+
try {
|
|
22
|
+
if (!fs.existsSync(filePath)) {
|
|
23
|
+
// Fall back to default if named profile doesn't exist
|
|
24
|
+
if (profileName !== "default" && fs.existsSync(DEFAULT_CONFIG)) {
|
|
25
|
+
return null; // Don't fall back silently — profile was explicitly requested
|
|
26
|
+
}
|
|
27
|
+
if (!fs.existsSync(DEFAULT_CONFIG))
|
|
28
|
+
return null;
|
|
29
|
+
const raw = fs.readFileSync(DEFAULT_CONFIG, "utf-8");
|
|
30
|
+
return JSON.parse(raw);
|
|
31
|
+
}
|
|
32
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
33
|
+
return JSON.parse(raw);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Save a named profile.
|
|
41
|
+
*/
|
|
42
|
+
export function saveProfile(profile, name) {
|
|
43
|
+
const profileName = name || "default";
|
|
44
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
45
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
46
|
+
}
|
|
47
|
+
if (profileName !== "default" && !fs.existsSync(PROFILES_DIR)) {
|
|
48
|
+
fs.mkdirSync(PROFILES_DIR, { recursive: true, mode: 0o700 });
|
|
49
|
+
}
|
|
50
|
+
const filePath = getProfilePath(profileName);
|
|
51
|
+
fs.writeFileSync(filePath, JSON.stringify(profile, null, 2) + "\n", {
|
|
52
|
+
mode: 0o600,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* List all saved profiles.
|
|
57
|
+
*/
|
|
58
|
+
export function listProfiles() {
|
|
59
|
+
const profiles = [];
|
|
60
|
+
// Default profile
|
|
61
|
+
if (fs.existsSync(DEFAULT_CONFIG)) {
|
|
62
|
+
try {
|
|
63
|
+
const raw = fs.readFileSync(DEFAULT_CONFIG, "utf-8");
|
|
64
|
+
profiles.push({ name: "default", profile: JSON.parse(raw) });
|
|
65
|
+
}
|
|
66
|
+
catch { /* skip corrupt */ }
|
|
67
|
+
}
|
|
68
|
+
// Named profiles
|
|
69
|
+
if (fs.existsSync(PROFILES_DIR)) {
|
|
70
|
+
for (const file of fs.readdirSync(PROFILES_DIR)) {
|
|
71
|
+
if (!file.endsWith(".json"))
|
|
72
|
+
continue;
|
|
73
|
+
const name = file.replace(/\.json$/, "");
|
|
74
|
+
try {
|
|
75
|
+
const raw = fs.readFileSync(path.join(PROFILES_DIR, file), "utf-8");
|
|
76
|
+
profiles.push({ name, profile: JSON.parse(raw) });
|
|
77
|
+
}
|
|
78
|
+
catch { /* skip corrupt */ }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return profiles;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Delete a named profile.
|
|
85
|
+
*/
|
|
86
|
+
export function deleteProfile(name) {
|
|
87
|
+
const filePath = getProfilePath(name);
|
|
88
|
+
if (fs.existsSync(filePath)) {
|
|
89
|
+
fs.unlinkSync(filePath);
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
export function profileExists(name) {
|
|
95
|
+
return fs.existsSync(getProfilePath(name || "default"));
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=profile.js.map
|
package/dist/retry.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exponential backoff retry utility for the daemon.
|
|
3
|
+
* Retries failed operations with increasing delays and jitter.
|
|
4
|
+
*/
|
|
5
|
+
interface RetryOptions {
|
|
6
|
+
maxRetries?: number;
|
|
7
|
+
baseDelayMs?: number;
|
|
8
|
+
maxDelayMs?: number;
|
|
9
|
+
onRetry?: (attempt: number, error: Error) => void;
|
|
10
|
+
}
|
|
11
|
+
export declare function withRetry<T>(fn: () => Promise<T>, options?: RetryOptions): Promise<T>;
|
|
12
|
+
/**
|
|
13
|
+
* Fetch wrapper with retry. Returns the Response object.
|
|
14
|
+
* Throws on non-OK responses so retry can catch them.
|
|
15
|
+
*/
|
|
16
|
+
export declare function fetchWithRetry(url: string, init: RequestInit, options?: RetryOptions): Promise<Response>;
|
|
17
|
+
export {};
|