@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/config.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import os from "os";
|
|
2
|
+
import { loadProfile } from "./profile.js";
|
|
3
|
+
export function loadConfig(overrides = {}) {
|
|
4
|
+
const profileName = overrides.profileName;
|
|
5
|
+
const profile = loadProfile(profileName);
|
|
6
|
+
return {
|
|
7
|
+
apiUrl: overrides.apiUrl ||
|
|
8
|
+
process.env.CLOUDCONTROL_API_URL ||
|
|
9
|
+
profile?.apiUrl ||
|
|
10
|
+
"http://localhost:3000",
|
|
11
|
+
apiKey: overrides.apiKey ||
|
|
12
|
+
process.env.CLOUDCONTROL_API_KEY ||
|
|
13
|
+
profile?.apiKey ||
|
|
14
|
+
"",
|
|
15
|
+
workerName: overrides.workerName ||
|
|
16
|
+
process.env.CLOUDCONTROL_WORKER_NAME ||
|
|
17
|
+
profile?.workerName ||
|
|
18
|
+
`worker-${os.hostname()}`,
|
|
19
|
+
workerType: overrides.workerType ||
|
|
20
|
+
process.env.CLOUDCONTROL_WORKER_TYPE ||
|
|
21
|
+
"daemon",
|
|
22
|
+
capabilities: overrides.capabilities ||
|
|
23
|
+
(process.env.CLOUDCONTROL_CAPABILITIES?.split(",") ?? [
|
|
24
|
+
"browser",
|
|
25
|
+
"filesystem",
|
|
26
|
+
"shell",
|
|
27
|
+
"ai_execution",
|
|
28
|
+
]),
|
|
29
|
+
taskTypeFilter: overrides.taskTypeFilter ||
|
|
30
|
+
(process.env.CLOUDCONTROL_TASK_TYPES?.split(",") ?? undefined),
|
|
31
|
+
pollInterval: overrides.pollInterval ||
|
|
32
|
+
parseInt(process.env.CLOUDCONTROL_POLL_INTERVAL || "15000"),
|
|
33
|
+
heartbeatInterval: overrides.heartbeatInterval || 30_000,
|
|
34
|
+
model: overrides.model || process.env.CLOUDCONTROL_MODEL,
|
|
35
|
+
profileName,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CloudControl MCP Server
|
|
4
|
+
*
|
|
5
|
+
* 5 consolidated tools for Claude Desktop:
|
|
6
|
+
* cloudcontrol — account: whoami, profiles, switch company
|
|
7
|
+
* cloudcontrol_tasks — list, get, create tasks
|
|
8
|
+
* cloudcontrol_work — claim, submit, add process note
|
|
9
|
+
* cloudcontrol_team — list workers, post activity
|
|
10
|
+
* cloudcontrol_search — search task history by type/title
|
|
11
|
+
*
|
|
12
|
+
* Usage in claude_desktop_config.json:
|
|
13
|
+
* {
|
|
14
|
+
* "mcpServers": {
|
|
15
|
+
* "cloudcontrol": {
|
|
16
|
+
* "command": "npx",
|
|
17
|
+
* "args": ["tsx", "/path/to/daemon/src/mcp-server.ts"],
|
|
18
|
+
* "env": {
|
|
19
|
+
* "CLOUDCONTROL_API_KEY": "cc_xxx",
|
|
20
|
+
* "CLOUDCONTROL_API_URL": "https://cloudcontrol.onrender.com"
|
|
21
|
+
* }
|
|
22
|
+
* }
|
|
23
|
+
* }
|
|
24
|
+
* }
|
|
25
|
+
*/
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CloudControl MCP Server
|
|
4
|
+
*
|
|
5
|
+
* 5 consolidated tools for Claude Desktop:
|
|
6
|
+
* cloudcontrol — account: whoami, profiles, switch company
|
|
7
|
+
* cloudcontrol_tasks — list, get, create tasks
|
|
8
|
+
* cloudcontrol_work — claim, submit, add process note
|
|
9
|
+
* cloudcontrol_team — list workers, post activity
|
|
10
|
+
* cloudcontrol_search — search task history by type/title
|
|
11
|
+
*
|
|
12
|
+
* Usage in claude_desktop_config.json:
|
|
13
|
+
* {
|
|
14
|
+
* "mcpServers": {
|
|
15
|
+
* "cloudcontrol": {
|
|
16
|
+
* "command": "npx",
|
|
17
|
+
* "args": ["tsx", "/path/to/daemon/src/mcp-server.ts"],
|
|
18
|
+
* "env": {
|
|
19
|
+
* "CLOUDCONTROL_API_KEY": "cc_xxx",
|
|
20
|
+
* "CLOUDCONTROL_API_URL": "https://cloudcontrol.onrender.com"
|
|
21
|
+
* }
|
|
22
|
+
* }
|
|
23
|
+
* }
|
|
24
|
+
* }
|
|
25
|
+
*/
|
|
26
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
27
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
28
|
+
import { z } from "zod";
|
|
29
|
+
import os from "os";
|
|
30
|
+
import { DAEMON_VERSION } from "./version.js";
|
|
31
|
+
import { loadProfile, listProfiles } from "./profile.js";
|
|
32
|
+
const profileName = process.env.CLOUDCONTROL_PROFILE;
|
|
33
|
+
const profile = profileName ? loadProfile(profileName) : null;
|
|
34
|
+
let API_URL = process.env.CLOUDCONTROL_API_URL || profile?.apiUrl || "http://localhost:3000";
|
|
35
|
+
let API_KEY = process.env.CLOUDCONTROL_API_KEY || profile?.apiKey || "";
|
|
36
|
+
let WORKER_NAME = process.env.CLOUDCONTROL_WORKER_NAME || profile?.workerName || `${os.hostname()}-mcp`;
|
|
37
|
+
let activeProfileName = profileName || "default";
|
|
38
|
+
if (!API_KEY) {
|
|
39
|
+
console.error("CLOUDCONTROL_API_KEY or CLOUDCONTROL_PROFILE required");
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
let workerId = null;
|
|
43
|
+
// ── Helpers ─────────────────────────────────────────
|
|
44
|
+
async function apiCall(method, path, body) {
|
|
45
|
+
const res = await fetch(`${API_URL}${path}`, {
|
|
46
|
+
method,
|
|
47
|
+
headers: { authorization: `Bearer ${API_KEY}`, "content-type": "application/json" },
|
|
48
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
49
|
+
});
|
|
50
|
+
const data = await res.json();
|
|
51
|
+
if (!res.ok)
|
|
52
|
+
throw new Error(`API ${res.status}: ${JSON.stringify(data)}`);
|
|
53
|
+
return data;
|
|
54
|
+
}
|
|
55
|
+
async function registerWorker() {
|
|
56
|
+
const data = await apiCall("POST", "/api/workers", {
|
|
57
|
+
name: WORKER_NAME,
|
|
58
|
+
workerType: "browser",
|
|
59
|
+
connectionMode: "manual",
|
|
60
|
+
platform: os.platform(),
|
|
61
|
+
capabilities: ["ai_execution", "code", "research"],
|
|
62
|
+
metadata: { arch: os.arch(), daemonVersion: DAEMON_VERSION, client: process.env.MCP_CLIENT_NAME || "claude-desktop", connectionType: "mcp" },
|
|
63
|
+
});
|
|
64
|
+
return data.worker.id;
|
|
65
|
+
}
|
|
66
|
+
async function sendHeartbeat() {
|
|
67
|
+
if (!workerId)
|
|
68
|
+
return;
|
|
69
|
+
try {
|
|
70
|
+
await apiCall("POST", "/api/workers/heartbeat", { workerId, status: "online" });
|
|
71
|
+
}
|
|
72
|
+
catch { }
|
|
73
|
+
}
|
|
74
|
+
let cachedPendingCount = 0;
|
|
75
|
+
let lastPendingCheck = 0;
|
|
76
|
+
async function getPendingCount() {
|
|
77
|
+
if (Date.now() - lastPendingCheck < 10_000)
|
|
78
|
+
return cachedPendingCount;
|
|
79
|
+
try {
|
|
80
|
+
const p = workerId ? `status=pending&limit=100&workerId=${workerId}` : "status=pending&limit=100";
|
|
81
|
+
const d = await apiCall("GET", `/api/tasks?${p}`);
|
|
82
|
+
cachedPendingCount = d.tasks.length;
|
|
83
|
+
lastPendingCheck = Date.now();
|
|
84
|
+
}
|
|
85
|
+
catch { }
|
|
86
|
+
return cachedPendingCount;
|
|
87
|
+
}
|
|
88
|
+
function withPending(text, count) {
|
|
89
|
+
return count > 0 ? `${text}\n\n---\n📋 ${count} pending task${count !== 1 ? "s" : ""} available.` : text;
|
|
90
|
+
}
|
|
91
|
+
async function resolveProcess(taskType) {
|
|
92
|
+
try {
|
|
93
|
+
const d = await apiCall("GET", `/api/processes/resolve?taskType=${encodeURIComponent(taskType)}`);
|
|
94
|
+
if (!d.parsed?.steps?.length)
|
|
95
|
+
return null;
|
|
96
|
+
const lines = [`\n## Process: ${d.parsed.name} v${d.parsed.version}\n`];
|
|
97
|
+
for (let i = 0; i < d.parsed.steps.length; i++) {
|
|
98
|
+
const s = d.parsed.steps[i];
|
|
99
|
+
let desc = `${i + 1}. [${s.type}] ${s.id}`;
|
|
100
|
+
if (s.prompt)
|
|
101
|
+
desc += ` — ${s.prompt}`;
|
|
102
|
+
if (s.gate === "approval_required")
|
|
103
|
+
desc += ` ⚠️ REQUIRES APPROVAL`;
|
|
104
|
+
lines.push(desc);
|
|
105
|
+
}
|
|
106
|
+
return lines.join("\n");
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function ok(text) { return { content: [{ type: "text", text }] }; }
|
|
113
|
+
/**
|
|
114
|
+
* Discover and save profiles for all teams accessible via the current API key.
|
|
115
|
+
* Uses GET /api/auth/teams with Bearer token — no email/password needed.
|
|
116
|
+
* Returns a list of lines describing what was found/saved.
|
|
117
|
+
*/
|
|
118
|
+
async function refreshProfiles() {
|
|
119
|
+
const { saveProfile: save } = await import("./profile.js");
|
|
120
|
+
const lines = [];
|
|
121
|
+
try {
|
|
122
|
+
const res = await fetch(`${API_URL}/api/auth/teams?generateKeys=true`, {
|
|
123
|
+
headers: { authorization: `Bearer ${API_KEY}` },
|
|
124
|
+
});
|
|
125
|
+
if (!res.ok) {
|
|
126
|
+
const err = await res.json().catch(() => ({}));
|
|
127
|
+
lines.push(`Refresh failed: ${err.error || res.status}`);
|
|
128
|
+
return lines;
|
|
129
|
+
}
|
|
130
|
+
const data = await res.json();
|
|
131
|
+
const existing = new Set(listProfiles().map((p) => p.name));
|
|
132
|
+
let newCount = 0;
|
|
133
|
+
for (const team of data.teams) {
|
|
134
|
+
const slug = team.teamName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
135
|
+
if (!existing.has(slug) && team.apiKey) {
|
|
136
|
+
save({ apiUrl: API_URL, apiKey: team.apiKey, workerName: WORKER_NAME, teamName: team.teamName }, slug);
|
|
137
|
+
lines.push(`✓ New: ${team.teamName} → profile "${slug}"`);
|
|
138
|
+
newCount++;
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
lines.push(` ${team.teamName} → already synced`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (newCount === 0) {
|
|
145
|
+
lines.unshift(`All ${data.teams.length} company(s) already synced.`);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
lines.unshift(`Found ${newCount} new company(s) (${data.teams.length} total):`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
lines.push(`Refresh error: ${err.message}`);
|
|
153
|
+
}
|
|
154
|
+
return lines;
|
|
155
|
+
}
|
|
156
|
+
// ── MCP Server ──────────────────────────────────────
|
|
157
|
+
const server = new McpServer({ name: "cloudcontrol", version: DAEMON_VERSION }, {
|
|
158
|
+
capabilities: { tools: {}, resources: {} },
|
|
159
|
+
instructions: "CloudControl task orchestration. Use cloudcontrol_tasks to list/create tasks, cloudcontrol_work to claim/submit, cloudcontrol to manage your account and company profiles.",
|
|
160
|
+
});
|
|
161
|
+
// ── Tool 1: cloudcontrol (account management) ───────
|
|
162
|
+
server.tool("cloudcontrol", "Account & COO: whoami, profiles, switch company, refresh (discover new companies), sync (email login), or talk to the COO agent", {
|
|
163
|
+
action: z.enum(["whoami", "profiles", "switch", "refresh", "sync", "coo"]).describe("Action: whoami, profiles, switch, refresh (discover new companies without password), sync (email login), coo (talk to COO agent)"),
|
|
164
|
+
profile: z.string().optional().describe("Profile name (for switch)"),
|
|
165
|
+
email: z.string().optional().describe("Email (for sync)"),
|
|
166
|
+
password: z.string().optional().describe("Password (for sync)"),
|
|
167
|
+
message: z.string().optional().describe("Message to the COO (for coo action — ask questions, give directives, request reports)"),
|
|
168
|
+
}, async ({ action, profile: targetProfile, email, password, message }) => {
|
|
169
|
+
if (action === "whoami") {
|
|
170
|
+
const pending = await getPendingCount();
|
|
171
|
+
return ok(withPending(JSON.stringify({ workerId, workerName: WORKER_NAME, activeProfile: activeProfileName, apiUrl: API_URL, platform: os.platform() }, null, 2), pending));
|
|
172
|
+
}
|
|
173
|
+
if (action === "profiles") {
|
|
174
|
+
const profiles = listProfiles();
|
|
175
|
+
if (profiles.length === 0)
|
|
176
|
+
return ok("No profiles saved. Run 'cloudcontrol login --profile <name>' in a terminal.");
|
|
177
|
+
const lines = profiles.map((p) => `${p.name === activeProfileName ? "→ " : " "}${p.name}: ${p.profile.teamName || "—"} (${p.profile.apiUrl})`);
|
|
178
|
+
return ok(`Company profiles:\n\n${lines.join("\n")}\n\nUse action=switch, profile=<name> to change.`);
|
|
179
|
+
}
|
|
180
|
+
if (action === "switch") {
|
|
181
|
+
if (!targetProfile)
|
|
182
|
+
return ok("Provide profile name. Use action=profiles to see available.");
|
|
183
|
+
const loaded = loadProfile(targetProfile);
|
|
184
|
+
if (!loaded) {
|
|
185
|
+
const avail = listProfiles().map((p) => p.name).join(", ");
|
|
186
|
+
return ok(`Profile "${targetProfile}" not found. Available: ${avail || "none"}`);
|
|
187
|
+
}
|
|
188
|
+
API_URL = loaded.apiUrl;
|
|
189
|
+
API_KEY = loaded.apiKey;
|
|
190
|
+
WORKER_NAME = loaded.workerName || `${os.hostname()}-mcp`;
|
|
191
|
+
activeProfileName = targetProfile;
|
|
192
|
+
try {
|
|
193
|
+
workerId = await registerWorker();
|
|
194
|
+
}
|
|
195
|
+
catch { }
|
|
196
|
+
cachedPendingCount = 0;
|
|
197
|
+
lastPendingCheck = 0;
|
|
198
|
+
const pending = await getPendingCount();
|
|
199
|
+
return ok(withPending(`✅ Switched to "${loaded.teamName || targetProfile}"\nWorker: ${WORKER_NAME}\nWorker ID: ${workerId}`, pending));
|
|
200
|
+
}
|
|
201
|
+
if (action === "refresh") {
|
|
202
|
+
const lines = await refreshProfiles();
|
|
203
|
+
lines.push(``, `Use action=profiles to see all, action=switch to change.`);
|
|
204
|
+
return ok(lines.join("\n"));
|
|
205
|
+
}
|
|
206
|
+
if (action === "sync") {
|
|
207
|
+
if (!email || !password)
|
|
208
|
+
return ok("Provide email and password to sync all your companies.");
|
|
209
|
+
try {
|
|
210
|
+
const res = await fetch(`${API_URL}/api/auth/teams`, {
|
|
211
|
+
method: "POST",
|
|
212
|
+
headers: { "content-type": "application/json" },
|
|
213
|
+
body: JSON.stringify({ email, password, generateKeys: true }),
|
|
214
|
+
});
|
|
215
|
+
if (!res.ok) {
|
|
216
|
+
const err = await res.json();
|
|
217
|
+
return ok(`Sync failed: ${err.error || res.status}`);
|
|
218
|
+
}
|
|
219
|
+
const data = await res.json();
|
|
220
|
+
const { saveProfile: save } = await import("./profile.js");
|
|
221
|
+
const lines = [`Synced ${data.teams.length} company(s):\n`];
|
|
222
|
+
for (const team of data.teams) {
|
|
223
|
+
const slug = team.teamName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
224
|
+
save({ apiUrl: API_URL, apiKey: team.apiKey, workerName: WORKER_NAME, teamName: team.teamName }, slug);
|
|
225
|
+
lines.push(`✓ ${team.teamName} → profile "${slug}" (${team.role})`);
|
|
226
|
+
}
|
|
227
|
+
if (data.teams.length > 0) {
|
|
228
|
+
save({ apiUrl: API_URL, apiKey: data.teams[0].apiKey, workerName: WORKER_NAME, teamName: data.teams[0].teamName });
|
|
229
|
+
}
|
|
230
|
+
lines.push(`\nUse action=profiles to see all, action=switch to change.`);
|
|
231
|
+
return ok(lines.join("\n"));
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
return ok(`Sync error: ${err.message}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (action === "coo") {
|
|
238
|
+
if (!message)
|
|
239
|
+
return ok("Provide a message for the COO. Ask about operations, give directives, or request reports.");
|
|
240
|
+
// Send CEO message
|
|
241
|
+
await apiCall("POST", "/api/dashboard/coo", { action: "message", content: message });
|
|
242
|
+
// Fetch recent COO responses (may include response to this message if COO loop ran, otherwise latest context)
|
|
243
|
+
const data = await apiCall("GET", "/api/dashboard/coo");
|
|
244
|
+
const recent = data.messages.slice(-5);
|
|
245
|
+
const goalLines = data.goals.map((g) => {
|
|
246
|
+
const pct = g.target > 0 ? Math.round((g.current / g.target) * 100) : 0;
|
|
247
|
+
return ` ${pct >= 100 ? "✅" : pct >= 70 ? "🟡" : "🔴"} ${g.metric}: ${g.current}/${g.target} ${g.unit || ""} (${pct}%)`;
|
|
248
|
+
});
|
|
249
|
+
const lines = [`Message sent to COO.`, ``];
|
|
250
|
+
if (goalLines.length > 0) {
|
|
251
|
+
lines.push(`**Current Goals:**`, ...goalLines, ``);
|
|
252
|
+
}
|
|
253
|
+
if (recent.length > 0) {
|
|
254
|
+
lines.push(`**Recent conversation:**`);
|
|
255
|
+
for (const m of recent) {
|
|
256
|
+
lines.push(`${m.role === "ceo" ? "👤 CEO" : "🤖 COO"}: ${m.content.slice(0, 300)}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
lines.push(``, `The COO will respond within ${5} minutes. Check back with action=coo (no message) to see the response.`);
|
|
260
|
+
return ok(lines.join("\n"));
|
|
261
|
+
}
|
|
262
|
+
return ok("Unknown action. Use: whoami, profiles, switch, sync, or coo.");
|
|
263
|
+
});
|
|
264
|
+
// ── Tool 2: cloudcontrol_tasks (list, get, create) ──
|
|
265
|
+
server.tool("cloudcontrol_tasks", "Manage tasks: list tasks (filter by status/type), get full task details, or create a new task", {
|
|
266
|
+
action: z.enum(["list", "get", "create"]).describe("Action: list, get (by ID), or create"),
|
|
267
|
+
taskId: z.string().optional().describe("Task UUID (for action=get)"),
|
|
268
|
+
status: z.string().optional().describe("Filter by status: pending, claimed, running, completed, failed, human_required (for action=list)"),
|
|
269
|
+
taskType: z.string().optional().describe("Filter by task type (for action=list) or set type (for action=create)"),
|
|
270
|
+
limit: z.number().optional().describe("Max results for list (default 20)"),
|
|
271
|
+
title: z.string().optional().describe("Task title (for action=create)"),
|
|
272
|
+
description: z.string().optional().describe("Task description (for action=create)"),
|
|
273
|
+
priority: z.enum(["low", "normal", "high", "urgent"]).optional().describe("Priority (for action=create)"),
|
|
274
|
+
modelHint: z.string().optional().describe("AI model hint (for action=create)"),
|
|
275
|
+
workspace: z.string().optional().describe("Workspace slug (for action=create)"),
|
|
276
|
+
successCriteria: z.string().optional().describe("What does done look like (for action=create)"),
|
|
277
|
+
context: z.string().optional().describe("JSON context (for action=create)"),
|
|
278
|
+
}, async ({ action, taskId, status, taskType, limit, title, description, priority, modelHint, workspace, successCriteria, context: ctxStr }) => {
|
|
279
|
+
if (action === "list") {
|
|
280
|
+
const params = new URLSearchParams();
|
|
281
|
+
if (status)
|
|
282
|
+
params.set("status", status);
|
|
283
|
+
if (limit)
|
|
284
|
+
params.set("limit", String(limit));
|
|
285
|
+
else
|
|
286
|
+
params.set("limit", "20");
|
|
287
|
+
if (workerId)
|
|
288
|
+
params.set("workerId", workerId);
|
|
289
|
+
const d = await apiCall("GET", `/api/tasks?${params}`);
|
|
290
|
+
let tasks = d.tasks;
|
|
291
|
+
if (taskType)
|
|
292
|
+
tasks = tasks.filter((t) => t.taskType === taskType);
|
|
293
|
+
return ok(JSON.stringify(tasks.map((t) => ({ id: t.id, title: t.title, status: t.status, taskType: t.taskType, priority: t.priority, modelHint: t.modelHint, workspace: t.workspace, description: typeof t.description === "string" ? t.description?.slice(0, 200) : null })), null, 2));
|
|
294
|
+
}
|
|
295
|
+
if (action === "get") {
|
|
296
|
+
if (!taskId)
|
|
297
|
+
return ok("Provide taskId.");
|
|
298
|
+
const d = await apiCall("GET", `/api/tasks/${taskId}`);
|
|
299
|
+
return ok(JSON.stringify(d.task, null, 2));
|
|
300
|
+
}
|
|
301
|
+
if (action === "create") {
|
|
302
|
+
if (!title)
|
|
303
|
+
return ok("Provide title.");
|
|
304
|
+
let context;
|
|
305
|
+
if (ctxStr) {
|
|
306
|
+
try {
|
|
307
|
+
context = JSON.parse(ctxStr);
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
context = { raw: ctxStr };
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const d = await apiCall("POST", "/api/tasks", { title, description, taskType, priority: priority || "normal", modelHint, workspace, successCriteria, context, delegatedBy: workerId });
|
|
314
|
+
const pending = await getPendingCount();
|
|
315
|
+
return ok(withPending(`Task created: ${d.task.title} (${d.task.id})`, pending));
|
|
316
|
+
}
|
|
317
|
+
return ok("Unknown action. Use: list, get, or create.");
|
|
318
|
+
});
|
|
319
|
+
// ── Tool 3: cloudcontrol_work (claim, submit, note) ─
|
|
320
|
+
server.tool("cloudcontrol_work", "Execute tasks: claim a task (get full context + process steps), submit results, or add a process improvement note", {
|
|
321
|
+
action: z.enum(["claim", "submit", "note"]).describe("Action: claim (reserve task), submit (complete task), note (add process note)"),
|
|
322
|
+
taskId: z.string().describe("Task UUID"),
|
|
323
|
+
status: z.enum(["completed", "failed", "human_required"]).optional().describe("Outcome status (for action=submit)"),
|
|
324
|
+
result: z.string().optional().describe("Result text or JSON (for action=submit)"),
|
|
325
|
+
dialogue: z.string().optional().describe("Your reasoning and approach (for action=submit)"),
|
|
326
|
+
processNote: z.string().optional().describe("Advice for future workers (for action=submit or action=note)"),
|
|
327
|
+
humanContext: z.string().optional().describe("Why human needed (for action=submit with status=human_required)"),
|
|
328
|
+
}, async ({ action, taskId, status, result, dialogue, processNote, humanContext }) => {
|
|
329
|
+
if (!workerId)
|
|
330
|
+
return ok("Worker not registered. Restart the MCP server.");
|
|
331
|
+
if (action === "claim") {
|
|
332
|
+
const d = await apiCall("POST", `/api/tasks/${taskId}/claim`, { workerId });
|
|
333
|
+
const task = d.task;
|
|
334
|
+
const lines = [
|
|
335
|
+
`✅ Task claimed.`,
|
|
336
|
+
``,
|
|
337
|
+
`**${task.title}**`,
|
|
338
|
+
`ID: ${task.id} | Type: ${task.taskType || "general"} | Priority: ${task.priority}`,
|
|
339
|
+
];
|
|
340
|
+
if (task.description)
|
|
341
|
+
lines.push(``, `## Description`, String(task.description));
|
|
342
|
+
if (task.context)
|
|
343
|
+
lines.push(``, `## Context`, `\`\`\`json`, JSON.stringify(task.context, null, 2), `\`\`\``);
|
|
344
|
+
if (task.humanContext)
|
|
345
|
+
lines.push(``, `## Previous Feedback`, String(task.humanContext));
|
|
346
|
+
if (task.processHint)
|
|
347
|
+
lines.push(``, `## Process Hint`, String(task.processHint));
|
|
348
|
+
// Workspace context
|
|
349
|
+
if (task.workspace) {
|
|
350
|
+
try {
|
|
351
|
+
const ws = await apiCall("GET", `/api/workspaces?slug=${encodeURIComponent(String(task.workspace))}`);
|
|
352
|
+
if (ws.found && ws.workspace) {
|
|
353
|
+
lines.push(``, `## Workspace: ${ws.workspace.name}`);
|
|
354
|
+
if (ws.workspace.description)
|
|
355
|
+
lines.push(ws.workspace.description);
|
|
356
|
+
if (ws.workspace.context)
|
|
357
|
+
lines.push(``, `### Context`, ws.workspace.context);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
catch { }
|
|
361
|
+
}
|
|
362
|
+
// Process steps + history
|
|
363
|
+
if (task.taskType) {
|
|
364
|
+
const proc = await resolveProcess(String(task.taskType));
|
|
365
|
+
if (proc)
|
|
366
|
+
lines.push(proc);
|
|
367
|
+
try {
|
|
368
|
+
const hp = new URLSearchParams();
|
|
369
|
+
hp.set("taskType", String(task.taskType));
|
|
370
|
+
if (task.title)
|
|
371
|
+
hp.set("title", String(task.title));
|
|
372
|
+
const h = await apiCall("GET", `/api/tasks/history?${hp}`);
|
|
373
|
+
if (h.found && h.lastCompleted) {
|
|
374
|
+
lines.push(``, `## Prior Run`);
|
|
375
|
+
lines.push(`Last: "${h.lastCompleted.title}" ${h.matchCount && h.matchCount > 1 ? `(${h.matchCount} runs found)` : ""}`);
|
|
376
|
+
if (h.lastCompleted.resultSummary)
|
|
377
|
+
lines.push(`Result: ${h.lastCompleted.resultSummary.slice(0, 300)}`);
|
|
378
|
+
if (h.processNotes?.length) {
|
|
379
|
+
lines.push(``, `### Process Notes`);
|
|
380
|
+
for (const n of h.processNotes)
|
|
381
|
+
lines.push(n);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
catch { }
|
|
386
|
+
}
|
|
387
|
+
if (task.successCriteria)
|
|
388
|
+
lines.push(``, `## Success Criteria`, String(task.successCriteria));
|
|
389
|
+
else
|
|
390
|
+
lines.push(``, `⚠️ No success criteria defined.`);
|
|
391
|
+
// Dependencies
|
|
392
|
+
if (task.parentTaskId) {
|
|
393
|
+
try {
|
|
394
|
+
const p = await apiCall("GET", `/api/tasks/${task.parentTaskId}`);
|
|
395
|
+
lines.push(``, `## Parent: "${p.task.title}" (${p.task.status})`);
|
|
396
|
+
}
|
|
397
|
+
catch { }
|
|
398
|
+
}
|
|
399
|
+
try {
|
|
400
|
+
const ch = await apiCall("GET", `/api/tasks?parentTaskId=${task.id}&limit=20`);
|
|
401
|
+
if (ch.tasks.length > 0) {
|
|
402
|
+
lines.push(``, `## Sub-tasks (${ch.tasks.length})`);
|
|
403
|
+
for (const c of ch.tasks)
|
|
404
|
+
lines.push(`${c.status === "completed" ? "✅" : c.status === "failed" ? "❌" : "⏳"} ${c.title} (${c.status})`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
catch { }
|
|
408
|
+
lines.push(``, `---`, `When done, use cloudcontrol_work action=submit.`);
|
|
409
|
+
const pending = await getPendingCount();
|
|
410
|
+
return ok(withPending(lines.join("\n"), pending));
|
|
411
|
+
}
|
|
412
|
+
if (action === "submit") {
|
|
413
|
+
if (!status)
|
|
414
|
+
return ok("Provide status: completed, failed, or human_required.");
|
|
415
|
+
let resultObj = {};
|
|
416
|
+
if (result) {
|
|
417
|
+
try {
|
|
418
|
+
resultObj = JSON.parse(result);
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
resultObj = { output: result };
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
const dialogueArr = dialogue ? [{ role: "assistant", content: dialogue, timestamp: new Date().toISOString() }] : undefined;
|
|
425
|
+
const d = await apiCall("POST", `/api/tasks/${taskId}/submit`, { workerId, status, result: resultObj, dialogue: dialogueArr, humanContext, metadata: { source: "mcp" } });
|
|
426
|
+
if (processNote) {
|
|
427
|
+
try {
|
|
428
|
+
await apiCall("POST", `/api/tasks/${taskId}/activity`, { activityType: "comment", content: `📝 Process note for "${d.task.taskType || "general"}" tasks:\n${processNote}`, workerId, metadata: { type: "process_note" } });
|
|
429
|
+
}
|
|
430
|
+
catch { }
|
|
431
|
+
}
|
|
432
|
+
const pending = await getPendingCount();
|
|
433
|
+
const lines = [`Task submitted as ${status}. ${JSON.stringify({ id: d.task.id, title: d.task.title }, null, 2)}`];
|
|
434
|
+
if (processNote)
|
|
435
|
+
lines.push(`\n✅ Process note saved.`);
|
|
436
|
+
else
|
|
437
|
+
lines.push(`\n💡 Add a process note? Use action=note to help future workers.`);
|
|
438
|
+
return ok(withPending(lines.join(""), pending));
|
|
439
|
+
}
|
|
440
|
+
if (action === "note") {
|
|
441
|
+
if (!processNote)
|
|
442
|
+
return ok("Provide processNote with your advice for future workers.");
|
|
443
|
+
const td = await apiCall("GET", `/api/tasks/${taskId}`);
|
|
444
|
+
await apiCall("POST", `/api/tasks/${taskId}/activity`, { activityType: "comment", content: `📝 Process note for "${td.task.taskType || "general"}" tasks:\n${processNote}`, workerId, metadata: { type: "process_note" } });
|
|
445
|
+
return ok(`✅ Process note saved for "${td.task.taskType || "general"}" tasks.`);
|
|
446
|
+
}
|
|
447
|
+
return ok("Unknown action. Use: claim, submit, or note.");
|
|
448
|
+
});
|
|
449
|
+
// ── Tool 4: cloudcontrol_team (workers, activity) ───
|
|
450
|
+
server.tool("cloudcontrol_team", "Team management: list workers or post a progress update on a task", {
|
|
451
|
+
action: z.enum(["workers", "activity"]).describe("Action: workers (list team), activity (post update on task)"),
|
|
452
|
+
taskId: z.string().optional().describe("Task UUID (for action=activity)"),
|
|
453
|
+
content: z.string().optional().describe("Progress update text (for action=activity)"),
|
|
454
|
+
}, async ({ action, taskId, content: activityContent }) => {
|
|
455
|
+
if (action === "workers") {
|
|
456
|
+
const d = await apiCall("GET", "/api/workers");
|
|
457
|
+
return ok(JSON.stringify(d.workers.map((w) => ({
|
|
458
|
+
id: w.id, name: w.name, workerType: w.workerType, status: w.status,
|
|
459
|
+
capabilities: w.capabilities, isYou: w.id === workerId,
|
|
460
|
+
})), null, 2));
|
|
461
|
+
}
|
|
462
|
+
if (action === "activity") {
|
|
463
|
+
if (!taskId || !activityContent)
|
|
464
|
+
return ok("Provide taskId and content.");
|
|
465
|
+
await apiCall("POST", `/api/tasks/${taskId}/activity`, { activityType: "progress", content: activityContent, workerId });
|
|
466
|
+
const pending = await getPendingCount();
|
|
467
|
+
return ok(withPending(`Activity posted on task ${taskId}.`, pending));
|
|
468
|
+
}
|
|
469
|
+
return ok("Unknown action. Use: workers or activity.");
|
|
470
|
+
});
|
|
471
|
+
// ── Tool 5: cloudcontrol_search (task history) ──────
|
|
472
|
+
server.tool("cloudcontrol_search", "Search task history: find prior completed runs by task type or title for institutional memory", {
|
|
473
|
+
taskType: z.string().optional().describe("Task type to search (e.g., research, test.e2e, directory_submission)"),
|
|
474
|
+
title: z.string().optional().describe("Task title keywords for similarity matching"),
|
|
475
|
+
}, async ({ taskType, title }) => {
|
|
476
|
+
if (!taskType && !title)
|
|
477
|
+
return ok("Provide taskType or title to search.");
|
|
478
|
+
const params = new URLSearchParams();
|
|
479
|
+
if (taskType)
|
|
480
|
+
params.set("taskType", taskType);
|
|
481
|
+
if (title)
|
|
482
|
+
params.set("title", title);
|
|
483
|
+
const d = await apiCall("GET", `/api/tasks/history?${params}`);
|
|
484
|
+
return ok(JSON.stringify(d, null, 2));
|
|
485
|
+
});
|
|
486
|
+
// ── Resources ───────────────────────────────────────
|
|
487
|
+
server.resource("cloudcontrol://tasks/pending", "cloudcontrol://tasks/pending", async () => {
|
|
488
|
+
const p = workerId ? `status=pending&limit=20&workerId=${workerId}` : "status=pending&limit=20";
|
|
489
|
+
const d = await apiCall("GET", `/api/tasks?${p}`);
|
|
490
|
+
return { contents: [{ uri: "cloudcontrol://tasks/pending", mimeType: "application/json", text: JSON.stringify(d.tasks, null, 2) }] };
|
|
491
|
+
});
|
|
492
|
+
server.resource("cloudcontrol://health", "cloudcontrol://health", async () => {
|
|
493
|
+
const res = await fetch(`${API_URL}/api/health`);
|
|
494
|
+
const d = await res.json();
|
|
495
|
+
return { contents: [{ uri: "cloudcontrol://health", mimeType: "application/json", text: JSON.stringify(d, null, 2) }] };
|
|
496
|
+
});
|
|
497
|
+
// ── Start ───────────────────────────────────────────
|
|
498
|
+
async function main() {
|
|
499
|
+
try {
|
|
500
|
+
workerId = await registerWorker();
|
|
501
|
+
console.error(`[mcp] Registered as worker ${workerId} (${WORKER_NAME})`);
|
|
502
|
+
}
|
|
503
|
+
catch (err) {
|
|
504
|
+
console.error(`[mcp] Worker registration failed: ${err.message}`);
|
|
505
|
+
}
|
|
506
|
+
// Auto-discover new companies on startup (non-blocking)
|
|
507
|
+
refreshProfiles().then((lines) => {
|
|
508
|
+
const newLines = lines.filter((l) => l.startsWith("✓"));
|
|
509
|
+
if (newLines.length > 0) {
|
|
510
|
+
console.error(`[mcp] Auto-refresh: discovered ${newLines.length} new company(s)`);
|
|
511
|
+
for (const l of newLines)
|
|
512
|
+
console.error(`[mcp] ${l}`);
|
|
513
|
+
}
|
|
514
|
+
}).catch(() => { });
|
|
515
|
+
const heartbeatTimer = setInterval(sendHeartbeat, 30_000);
|
|
516
|
+
const transport = new StdioServerTransport();
|
|
517
|
+
await server.connect(transport);
|
|
518
|
+
console.error("[mcp] CloudControl MCP server running");
|
|
519
|
+
process.on("SIGINT", () => { clearInterval(heartbeatTimer); process.exit(0); });
|
|
520
|
+
}
|
|
521
|
+
main().catch((err) => { console.error("[mcp] Fatal:", err); process.exit(1); });
|
|
522
|
+
//# sourceMappingURL=mcp-server.js.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type ExecutionResult } from "./models/claude.js";
|
|
2
|
+
export interface ModelAdapter {
|
|
3
|
+
execute(task: TaskInput): Promise<ExecutionResult>;
|
|
4
|
+
}
|
|
5
|
+
export interface TaskInput {
|
|
6
|
+
title: string;
|
|
7
|
+
description?: string | null;
|
|
8
|
+
taskType?: string | null;
|
|
9
|
+
context?: Record<string, unknown> | null;
|
|
10
|
+
processHint?: string | null;
|
|
11
|
+
humanContext?: string | null;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* ModelRouter manages a roster of available AI models and selects
|
|
15
|
+
* the best one for each task based on the task's modelHint.
|
|
16
|
+
*
|
|
17
|
+
* Models are detected from:
|
|
18
|
+
* 1. ANTHROPIC_API_KEY → Claude API (sonnet + haiku)
|
|
19
|
+
* 2. Installed CLIs → auto-detected from KNOWN_CLIS + CLOUDCONTROL_CLI_MODELS env
|
|
20
|
+
* 3. CLOUDCONTROL_OLLAMA_MODELS → Ollama local models
|
|
21
|
+
*/
|
|
22
|
+
export declare class ModelRouter {
|
|
23
|
+
private models;
|
|
24
|
+
private defaultModel;
|
|
25
|
+
constructor();
|
|
26
|
+
private detectAvailableModels;
|
|
27
|
+
/**
|
|
28
|
+
* Select the best model for a task based on its modelHint.
|
|
29
|
+
*/
|
|
30
|
+
select(modelHint?: string | null): {
|
|
31
|
+
adapter: ModelAdapter;
|
|
32
|
+
name: string;
|
|
33
|
+
};
|
|
34
|
+
private getByName;
|
|
35
|
+
getDefault(): string;
|
|
36
|
+
listModels(): Array<{
|
|
37
|
+
name: string;
|
|
38
|
+
traits: string[];
|
|
39
|
+
}>;
|
|
40
|
+
}
|