@getforgeai/cli 0.1.0-beta.1
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/LICENSE +201 -0
- package/README.md +80 -0
- package/dist/bin/forgeai.d.ts +3 -0
- package/dist/bin/forgeai.js +8 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +180 -0
- package/dist/commands/auth-login.d.ts +4 -0
- package/dist/commands/auth-login.js +202 -0
- package/dist/commands/auth-logout.d.ts +2 -0
- package/dist/commands/auth-logout.js +41 -0
- package/dist/commands/auth-status.d.ts +4 -0
- package/dist/commands/auth-status.js +81 -0
- package/dist/commands/cli-list.d.ts +4 -0
- package/dist/commands/cli-list.js +40 -0
- package/dist/commands/cli-unlink.d.ts +4 -0
- package/dist/commands/cli-unlink.js +45 -0
- package/dist/commands/config-set.d.ts +5 -0
- package/dist/commands/config-set.js +63 -0
- package/dist/commands/connect.d.ts +9 -0
- package/dist/commands/connect.js +165 -0
- package/dist/commands/disconnect.d.ts +5 -0
- package/dist/commands/disconnect.js +126 -0
- package/dist/commands/journal-clear.d.ts +2 -0
- package/dist/commands/journal-clear.js +16 -0
- package/dist/commands/journal-list.d.ts +2 -0
- package/dist/commands/journal-list.js +20 -0
- package/dist/commands/journal-status.d.ts +2 -0
- package/dist/commands/journal-status.js +18 -0
- package/dist/commands/mcp-serve.d.ts +13 -0
- package/dist/commands/mcp-serve.js +74 -0
- package/dist/commands/org-list.d.ts +4 -0
- package/dist/commands/org-list.js +56 -0
- package/dist/commands/org-unlink.d.ts +4 -0
- package/dist/commands/org-unlink.js +63 -0
- package/dist/commands/reconnect.d.ts +4 -0
- package/dist/commands/reconnect.js +46 -0
- package/dist/commands/release-open.d.ts +2 -0
- package/dist/commands/release-open.js +43 -0
- package/dist/commands/session-respond.d.ts +2 -0
- package/dist/commands/session-respond.js +29 -0
- package/dist/commands/task-open.d.ts +2 -0
- package/dist/commands/task-open.js +42 -0
- package/dist/commands/vm.d.ts +21 -0
- package/dist/commands/vm.js +122 -0
- package/dist/commands/workflow-open.d.ts +2 -0
- package/dist/commands/workflow-open.js +43 -0
- package/dist/config/config-manager.d.ts +31 -0
- package/dist/config/config-manager.js +70 -0
- package/dist/config/token-manager.d.ts +11 -0
- package/dist/config/token-manager.js +87 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +22 -0
- package/dist/lib/auth-client.d.ts +1670 -0
- package/dist/lib/auth-client.js +10 -0
- package/dist/lib/branch-manager.d.ts +79 -0
- package/dist/lib/branch-manager.js +620 -0
- package/dist/lib/clone-and-checkout.d.ts +6 -0
- package/dist/lib/clone-and-checkout.js +41 -0
- package/dist/lib/connection-manager.d.ts +58 -0
- package/dist/lib/connection-manager.js +436 -0
- package/dist/lib/daemon-entry.d.ts +2 -0
- package/dist/lib/daemon-entry.js +210 -0
- package/dist/lib/daemon.d.ts +38 -0
- package/dist/lib/daemon.js +148 -0
- package/dist/lib/event-journal.d.ts +24 -0
- package/dist/lib/event-journal.js +76 -0
- package/dist/lib/ipc-client.d.ts +10 -0
- package/dist/lib/ipc-client.js +70 -0
- package/dist/lib/ipc-server.d.ts +36 -0
- package/dist/lib/ipc-server.js +70 -0
- package/dist/lib/node-version-guard.d.ts +8 -0
- package/dist/lib/node-version-guard.js +60 -0
- package/dist/lib/open-directory.d.ts +7 -0
- package/dist/lib/open-directory.js +41 -0
- package/dist/lib/repo-manager.d.ts +29 -0
- package/dist/lib/repo-manager.js +103 -0
- package/dist/lib/resilient-emitter.d.ts +53 -0
- package/dist/lib/resilient-emitter.js +160 -0
- package/dist/lib/rest-client.d.ts +164 -0
- package/dist/lib/rest-client.js +139 -0
- package/dist/lib/skill-manager.d.ts +23 -0
- package/dist/lib/skill-manager.js +167 -0
- package/dist/lib/socket-client.d.ts +68 -0
- package/dist/lib/socket-client.js +160 -0
- package/dist/mcp/mcp-probe.d.ts +11 -0
- package/dist/mcp/mcp-probe.js +106 -0
- package/dist/mcp/mcp-response.d.ts +29 -0
- package/dist/mcp/mcp-response.js +67 -0
- package/dist/mcp/mcp-server.d.ts +19 -0
- package/dist/mcp/mcp-server.js +76 -0
- package/dist/mcp/resources/project-conventions.d.ts +4 -0
- package/dist/mcp/resources/project-conventions.js +50 -0
- package/dist/mcp/resources/task-current.d.ts +4 -0
- package/dist/mcp/resources/task-current.js +51 -0
- package/dist/mcp/tools/forge-add-group-dependency.d.ts +4 -0
- package/dist/mcp/tools/forge-add-group-dependency.js +20 -0
- package/dist/mcp/tools/forge-add-task-dependency.d.ts +4 -0
- package/dist/mcp/tools/forge-add-task-dependency.js +20 -0
- package/dist/mcp/tools/forge-ask-human.d.ts +4 -0
- package/dist/mcp/tools/forge-ask-human.js +80 -0
- package/dist/mcp/tools/forge-column-done.d.ts +4 -0
- package/dist/mcp/tools/forge-column-done.js +46 -0
- package/dist/mcp/tools/forge-complete-step.d.ts +4 -0
- package/dist/mcp/tools/forge-complete-step.js +139 -0
- package/dist/mcp/tools/forge-create-task-group.d.ts +4 -0
- package/dist/mcp/tools/forge-create-task-group.js +22 -0
- package/dist/mcp/tools/forge-create-task.d.ts +4 -0
- package/dist/mcp/tools/forge-create-task.js +28 -0
- package/dist/mcp/tools/forge-delete-task-group.d.ts +4 -0
- package/dist/mcp/tools/forge-delete-task-group.js +17 -0
- package/dist/mcp/tools/forge-delete-task.d.ts +4 -0
- package/dist/mcp/tools/forge-delete-task.js +17 -0
- package/dist/mcp/tools/forge-get-task.d.ts +4 -0
- package/dist/mcp/tools/forge-get-task.js +17 -0
- package/dist/mcp/tools/forge-merge-back.d.ts +4 -0
- package/dist/mcp/tools/forge-merge-back.js +179 -0
- package/dist/mcp/tools/forge-release-merge.d.ts +4 -0
- package/dist/mcp/tools/forge-release-merge.js +159 -0
- package/dist/mcp/tools/forge-release-notes.d.ts +4 -0
- package/dist/mcp/tools/forge-release-notes.js +51 -0
- package/dist/mcp/tools/forge-release-tag.d.ts +4 -0
- package/dist/mcp/tools/forge-release-tag.js +125 -0
- package/dist/mcp/tools/forge-remove-group-dependency.d.ts +4 -0
- package/dist/mcp/tools/forge-remove-group-dependency.js +19 -0
- package/dist/mcp/tools/forge-remove-task-dependency.d.ts +4 -0
- package/dist/mcp/tools/forge-remove-task-dependency.js +19 -0
- package/dist/mcp/tools/forge-requeue.d.ts +4 -0
- package/dist/mcp/tools/forge-requeue.js +39 -0
- package/dist/mcp/tools/forge-submit-pr.d.ts +4 -0
- package/dist/mcp/tools/forge-submit-pr.js +68 -0
- package/dist/mcp/tools/forge-update-status.d.ts +4 -0
- package/dist/mcp/tools/forge-update-status.js +77 -0
- package/dist/mcp/tools/forge-update-task-group.d.ts +4 -0
- package/dist/mcp/tools/forge-update-task-group.js +21 -0
- package/dist/mcp/tools/forge-update-task.d.ts +4 -0
- package/dist/mcp/tools/forge-update-task.js +25 -0
- package/dist/mcp/tools/forge-validate-merge.d.ts +4 -0
- package/dist/mcp/tools/forge-validate-merge.js +194 -0
- package/dist/mcp/types.d.ts +39 -0
- package/dist/mcp/types.js +8 -0
- package/dist/methodology/artifact-manager.d.ts +14 -0
- package/dist/methodology/artifact-manager.js +55 -0
- package/dist/orchestrator/agent-registry.d.ts +53 -0
- package/dist/orchestrator/agent-registry.js +96 -0
- package/dist/orchestrator/agent-spawner.d.ts +112 -0
- package/dist/orchestrator/agent-spawner.js +1518 -0
- package/dist/orchestrator/connectors/claude-code-connector.d.ts +3 -0
- package/dist/orchestrator/connectors/claude-code-connector.js +60 -0
- package/dist/orchestrator/connectors/connector.d.ts +95 -0
- package/dist/orchestrator/connectors/connector.js +25 -0
- package/dist/orchestrator/connectors/docker-connector.d.ts +14 -0
- package/dist/orchestrator/connectors/docker-connector.js +337 -0
- package/dist/orchestrator/connectors/native-vm-connector.d.ts +11 -0
- package/dist/orchestrator/connectors/native-vm-connector.js +162 -0
- package/dist/orchestrator/connectors/opencode-connector.d.ts +3 -0
- package/dist/orchestrator/connectors/opencode-connector.js +39 -0
- package/dist/orchestrator/connectors/vm-connector.d.ts +3 -0
- package/dist/orchestrator/connectors/vm-connector.js +139 -0
- package/dist/orchestrator/dispatch-queue.d.ts +43 -0
- package/dist/orchestrator/dispatch-queue.js +93 -0
- package/dist/orchestrator/resume-handler.d.ts +34 -0
- package/dist/orchestrator/resume-handler.js +159 -0
- package/dist/orchestrator/stream-batcher.d.ts +16 -0
- package/dist/orchestrator/stream-batcher.js +50 -0
- package/dist/orchestrator/stream-json-extractor.d.ts +97 -0
- package/dist/orchestrator/stream-json-extractor.js +579 -0
- package/dist/vm/claude-token-manager.d.ts +36 -0
- package/dist/vm/claude-token-manager.js +223 -0
- package/dist/vm/docker-executor.d.ts +131 -0
- package/dist/vm/docker-executor.js +360 -0
- package/dist/vm/docker-process-proxy.d.ts +31 -0
- package/dist/vm/docker-process-proxy.js +105 -0
- package/dist/vm/forge-vm-agent.d.ts +11 -0
- package/dist/vm/forge-vm-agent.js +159 -0
- package/dist/vm/git-credentials.d.ts +43 -0
- package/dist/vm/git-credentials.js +53 -0
- package/dist/vm/image-manager.d.ts +7 -0
- package/dist/vm/image-manager.js +61 -0
- package/dist/vm/log.d.ts +9 -0
- package/dist/vm/log.js +18 -0
- package/dist/vm/mcp-bridge.d.ts +63 -0
- package/dist/vm/mcp-bridge.js +243 -0
- package/dist/vm/mcp-stub.d.ts +15 -0
- package/dist/vm/mcp-stub.js +67 -0
- package/dist/vm/native-addon.d.ts +69 -0
- package/dist/vm/native-addon.js +68 -0
- package/dist/vm/provider-resolver.d.ts +7 -0
- package/dist/vm/provider-resolver.js +27 -0
- package/dist/vm/providers/lima.d.ts +17 -0
- package/dist/vm/providers/lima.js +191 -0
- package/dist/vm/providers/qemu.d.ts +28 -0
- package/dist/vm/providers/qemu.js +260 -0
- package/dist/vm/providers/vfkit.d.ts +40 -0
- package/dist/vm/providers/vfkit.js +371 -0
- package/dist/vm/providers/wsl2.d.ts +27 -0
- package/dist/vm/providers/wsl2.js +226 -0
- package/dist/vm/session-manager.d.ts +130 -0
- package/dist/vm/session-manager.js +494 -0
- package/dist/vm/session-snapshot.d.ts +31 -0
- package/dist/vm/session-snapshot.js +180 -0
- package/dist/vm/types.d.ts +73 -0
- package/dist/vm/types.js +2 -0
- package/dist/vm/vm-helper-client.d.ts +81 -0
- package/dist/vm/vm-helper-client.js +335 -0
- package/dist/vm/vm-helper.d.ts +11 -0
- package/dist/vm/vm-helper.js +156 -0
- package/dist/vm/vm-ipc-protocol.d.ts +84 -0
- package/dist/vm/vm-ipc-protocol.js +9 -0
- package/dist/vm/vm-manager-facade.d.ts +23 -0
- package/dist/vm/vm-manager-facade.js +43 -0
- package/dist/vm/vm-manager.d.ts +74 -0
- package/dist/vm/vm-manager.js +515 -0
- package/dist/vm/vm-process-proxy.d.ts +34 -0
- package/dist/vm/vm-process-proxy.js +64 -0
- package/dist/vm/vsock-bridge.d.ts +44 -0
- package/dist/vm/vsock-bridge.js +140 -0
- package/dist/vm/vsock-protocol.d.ts +98 -0
- package/dist/vm/vsock-protocol.js +57 -0
- package/package.json +65 -0
- package/rootfs/Dockerfile +50 -0
- package/rootfs/forge-mcp-relay.mjs +158 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
2
|
+
const DEFAULT_MAX_RETRIES = 2;
|
|
3
|
+
const RETRY_BASE_DELAY_MS = 1_000;
|
|
4
|
+
function validateJsonResponse(json, status) {
|
|
5
|
+
if (json === null || typeof json !== "object") {
|
|
6
|
+
throw new RestClientError("Invalid response format from server", status);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export function createRestClient(serverUrl, token, orgId, options) {
|
|
10
|
+
const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
11
|
+
const maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
12
|
+
const retryDelayMs = options?.retryDelayMs ?? RETRY_BASE_DELAY_MS;
|
|
13
|
+
const baseHeaders = {
|
|
14
|
+
Authorization: `Bearer ${token}`,
|
|
15
|
+
"X-Org-Id": orgId,
|
|
16
|
+
"Content-Type": "application/json",
|
|
17
|
+
};
|
|
18
|
+
function handleResponseError(response, errorBody, attempt) {
|
|
19
|
+
if (response.status === 401) {
|
|
20
|
+
throw new RestClientError(`Authentication failed. Run "forge auth login" to re-authenticate.`, response.status);
|
|
21
|
+
}
|
|
22
|
+
if (response.status === 403) {
|
|
23
|
+
throw new RestClientError(errorBody ||
|
|
24
|
+
"Access denied. You may not be a member of this organization.", response.status);
|
|
25
|
+
}
|
|
26
|
+
if (response.status >= 500 && attempt < maxRetries) {
|
|
27
|
+
return new RestClientError(`Request failed: ${response.status} ${errorBody}`, response.status);
|
|
28
|
+
}
|
|
29
|
+
throw new RestClientError(`Request failed: ${response.status} ${errorBody}`, response.status);
|
|
30
|
+
}
|
|
31
|
+
function handleFetchError(error) {
|
|
32
|
+
if (error instanceof RestClientError)
|
|
33
|
+
throw error;
|
|
34
|
+
if (error instanceof DOMException && error.name === "AbortError") {
|
|
35
|
+
throw new RestClientError(`Request timed out after ${timeoutMs}ms`, 0);
|
|
36
|
+
}
|
|
37
|
+
throw new RestClientError(`Network error: ${error instanceof Error ? error.message : String(error)}`, 0);
|
|
38
|
+
}
|
|
39
|
+
async function request(method, path, body) {
|
|
40
|
+
let lastError;
|
|
41
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
42
|
+
if (attempt > 0) {
|
|
43
|
+
await sleep(retryDelayMs * attempt);
|
|
44
|
+
}
|
|
45
|
+
const controller = new AbortController();
|
|
46
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
47
|
+
try {
|
|
48
|
+
const response = await fetch(`${serverUrl}${path}`, {
|
|
49
|
+
method,
|
|
50
|
+
headers: baseHeaders,
|
|
51
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
52
|
+
signal: controller.signal,
|
|
53
|
+
});
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
const errorBody = await response.text().catch(() => "Unknown error");
|
|
56
|
+
const error = handleResponseError(response, errorBody, attempt);
|
|
57
|
+
if (error) {
|
|
58
|
+
lastError = error;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const json = await response.json();
|
|
63
|
+
validateJsonResponse(json, response.status);
|
|
64
|
+
return json;
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
handleFetchError(error);
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
clearTimeout(timeoutId);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
throw lastError ?? new RestClientError("Request failed after retries", 0);
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
heartbeat: (data) => request("POST", "/api/cli/heartbeat", data),
|
|
77
|
+
getTasks: () => request("GET", "/api/cli/tasks"),
|
|
78
|
+
getTask: (taskId) => request("GET", `/api/cli/tasks/${encodeURIComponent(taskId)}`),
|
|
79
|
+
updateTaskStatus: (data) => request("POST", "/api/cli/tasks", data),
|
|
80
|
+
updateTaskSubStatus: (data) => request("POST", "/api/cli/tasks", data),
|
|
81
|
+
requeueTask: (taskId, reason, errorReason, resetAt) => request("POST", `/api/cli/tasks/${encodeURIComponent(taskId)}/requeue`, { reason, errorReason, resetAt }),
|
|
82
|
+
advanceTask: (taskId, options) => request("POST", `/api/cli/tasks/${encodeURIComponent(taskId)}/advance${options?.fallback ? "?fallback=true" : ""}`),
|
|
83
|
+
respondToAgent: (taskId, response) => request("POST", `/api/cli/tasks/${encodeURIComponent(taskId)}/respond`, { response }),
|
|
84
|
+
syncEvents: (data) => request("POST", "/api/cli/sync", data),
|
|
85
|
+
registerCli: (data) => request("POST", "/api/cli/register", data),
|
|
86
|
+
updateCliConfig: (data) => request("PATCH", "/api/cli/register", data),
|
|
87
|
+
listCliDevices: () => request("GET", "/api/cli/devices"),
|
|
88
|
+
unlinkCli: (data) => request("DELETE", `/api/cli/register?cliId=${encodeURIComponent(data.cliId)}`),
|
|
89
|
+
disconnectCli: (data) => request("POST", "/api/cli/disconnect", data),
|
|
90
|
+
getProjectState: (projectId) => {
|
|
91
|
+
const query = projectId
|
|
92
|
+
? `?projectId=${encodeURIComponent(projectId)}`
|
|
93
|
+
: "";
|
|
94
|
+
return request("GET", `/api/cli/state${query}`);
|
|
95
|
+
},
|
|
96
|
+
getSkill: (skillName) => request("GET", `/api/cli/skills?name=${encodeURIComponent(skillName)}`),
|
|
97
|
+
getWorkflow: (workflowId) => request("GET", `/api/cli/workflows/${encodeURIComponent(workflowId)}`),
|
|
98
|
+
getRelease: (releaseId) => request("GET", `/api/cli/releases/${encodeURIComponent(releaseId)}`),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function sleep(ms) {
|
|
102
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
103
|
+
}
|
|
104
|
+
export async function getOrgSelection(serverUrl, token, userCode) {
|
|
105
|
+
const controller = new AbortController();
|
|
106
|
+
const timeoutId = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
|
|
107
|
+
try {
|
|
108
|
+
const response = await fetch(`${serverUrl}/api/cli/auth/org-selection?userCode=${encodeURIComponent(userCode)}`, {
|
|
109
|
+
headers: {
|
|
110
|
+
Authorization: `Bearer ${token}`,
|
|
111
|
+
"Content-Type": "application/json",
|
|
112
|
+
},
|
|
113
|
+
signal: controller.signal,
|
|
114
|
+
});
|
|
115
|
+
if (!response.ok) {
|
|
116
|
+
return { orgIds: [], orgSlugs: [] };
|
|
117
|
+
}
|
|
118
|
+
const json = (await response.json());
|
|
119
|
+
if (!json || !Array.isArray(json.orgIds) || !Array.isArray(json.orgSlugs)) {
|
|
120
|
+
return { orgIds: [], orgSlugs: [] };
|
|
121
|
+
}
|
|
122
|
+
return json;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return { orgIds: [], orgSlugs: [] };
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
clearTimeout(timeoutId);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
export class RestClientError extends Error {
|
|
132
|
+
status;
|
|
133
|
+
constructor(message, status) {
|
|
134
|
+
super(message);
|
|
135
|
+
this.name = "RestClientError";
|
|
136
|
+
this.status = status;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=rest-client.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { SkillPullResponse } from "@getforgeai/protocol";
|
|
2
|
+
type SkillRestClient = {
|
|
3
|
+
getSkill: (skillName: string) => Promise<unknown>;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Read a cached skill from disk.
|
|
7
|
+
*/
|
|
8
|
+
export declare function getCachedSkill(skillId: string, version: number): SkillPullResponse | null;
|
|
9
|
+
/**
|
|
10
|
+
* Remove all cached versions for a skill.
|
|
11
|
+
*/
|
|
12
|
+
export declare function clearSkillCache(skillId: string): void;
|
|
13
|
+
/**
|
|
14
|
+
* Pull a skill from Cloud API and cache it locally.
|
|
15
|
+
* Returns the skill data or null if unavailable.
|
|
16
|
+
*/
|
|
17
|
+
export declare function pullSkill(skillName: string, restClient: SkillRestClient): Promise<SkillPullResponse | null>;
|
|
18
|
+
/**
|
|
19
|
+
* Deposit skill files from cache into an agent's worktree.
|
|
20
|
+
*/
|
|
21
|
+
export declare function depositSkill(skill: SkillPullResponse, worktreePath: string, agentType: string): void;
|
|
22
|
+
export {};
|
|
23
|
+
//# sourceMappingURL=skill-manager.d.ts.map
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join, normalize } from "node:path";
|
|
4
|
+
import { SkillPullResponseSchema } from "@getforgeai/protocol";
|
|
5
|
+
import { RestClientError } from "./rest-client.js";
|
|
6
|
+
import { vmLog } from "../vm/log.js";
|
|
7
|
+
const CACHE_BASE = join(homedir(), ".forgeai", "cache", "skills");
|
|
8
|
+
function getCacheDir(skillId, version) {
|
|
9
|
+
return join(CACHE_BASE, `${skillId}-v${version}`);
|
|
10
|
+
}
|
|
11
|
+
function findExistingCacheVersion(skillId) {
|
|
12
|
+
if (!existsSync(CACHE_BASE))
|
|
13
|
+
return null;
|
|
14
|
+
const prefix = `${skillId}-v`;
|
|
15
|
+
const entries = readdirSync(CACHE_BASE, { withFileTypes: true });
|
|
16
|
+
for (const entry of entries) {
|
|
17
|
+
if (entry.isDirectory() && entry.name.startsWith(prefix)) {
|
|
18
|
+
const versionStr = entry.name.slice(prefix.length);
|
|
19
|
+
const version = Number.parseInt(versionStr, 10);
|
|
20
|
+
if (!Number.isNaN(version)) {
|
|
21
|
+
return { version, dir: join(CACHE_BASE, entry.name) };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Read a cached skill from disk.
|
|
29
|
+
*/
|
|
30
|
+
export function getCachedSkill(skillId, version) {
|
|
31
|
+
const cacheDir = getCacheDir(skillId, version);
|
|
32
|
+
const metadataPath = join(cacheDir, "skill.json");
|
|
33
|
+
if (!existsSync(metadataPath))
|
|
34
|
+
return null;
|
|
35
|
+
try {
|
|
36
|
+
const raw = readFileSync(metadataPath, "utf-8");
|
|
37
|
+
const parsed = JSON.parse(raw);
|
|
38
|
+
const result = SkillPullResponseSchema.safeParse(parsed);
|
|
39
|
+
return result.success ? result.data : null;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Remove all cached versions for a skill.
|
|
47
|
+
*/
|
|
48
|
+
export function clearSkillCache(skillId) {
|
|
49
|
+
if (!existsSync(CACHE_BASE))
|
|
50
|
+
return;
|
|
51
|
+
const prefix = `${skillId}-v`;
|
|
52
|
+
const entries = readdirSync(CACHE_BASE, { withFileTypes: true });
|
|
53
|
+
for (const entry of entries) {
|
|
54
|
+
if (entry.isDirectory() && entry.name.startsWith(prefix)) {
|
|
55
|
+
rmSync(join(CACHE_BASE, entry.name), { recursive: true, force: true });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function writeSkillToCache(skill) {
|
|
60
|
+
// Invalidate old version if exists
|
|
61
|
+
const existing = findExistingCacheVersion(skill.id);
|
|
62
|
+
if (existing && existing.version !== skill.version) {
|
|
63
|
+
rmSync(existing.dir, { recursive: true, force: true });
|
|
64
|
+
}
|
|
65
|
+
const cacheDir = getCacheDir(skill.id, skill.version);
|
|
66
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
67
|
+
// Write metadata
|
|
68
|
+
writeFileSync(join(cacheDir, "skill.json"), JSON.stringify(skill, null, 2), "utf-8");
|
|
69
|
+
// Write individual files
|
|
70
|
+
for (const file of skill.files) {
|
|
71
|
+
const safePath = normalize(file.path);
|
|
72
|
+
const resolved = join(cacheDir, safePath);
|
|
73
|
+
if (!resolved.startsWith(cacheDir + "/") && resolved !== cacheDir) {
|
|
74
|
+
continue; // prevent path traversal
|
|
75
|
+
}
|
|
76
|
+
mkdirSync(dirname(resolved), { recursive: true });
|
|
77
|
+
writeFileSync(resolved, file.content, "utf-8");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Pull a skill from Cloud API and cache it locally.
|
|
82
|
+
* Returns the skill data or null if unavailable.
|
|
83
|
+
*/
|
|
84
|
+
export async function pullSkill(skillName, restClient) {
|
|
85
|
+
try {
|
|
86
|
+
const rawResponse = await restClient.getSkill(skillName);
|
|
87
|
+
const result = SkillPullResponseSchema.safeParse(rawResponse);
|
|
88
|
+
if (!result.success) {
|
|
89
|
+
vmLog.warn("skill", `Invalid skill response for skill ${skillName}`);
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
const skill = result.data;
|
|
93
|
+
// Check cache — if same version exists, return cached
|
|
94
|
+
const cached = getCachedSkill(skill.id, skill.version);
|
|
95
|
+
if (cached) {
|
|
96
|
+
vmLog.dim("skill", `Cache hit: ${skill.name} v${skill.version}`);
|
|
97
|
+
return cached;
|
|
98
|
+
}
|
|
99
|
+
// Write to cache (handles version invalidation)
|
|
100
|
+
writeSkillToCache(skill);
|
|
101
|
+
vmLog.info("skill", `Pulled: ${skill.name} v${skill.version} (${skill.files.length} files)`);
|
|
102
|
+
return skill;
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (error instanceof RestClientError && error.status === 404) {
|
|
106
|
+
vmLog.dim("skill", `No skill found for skill ${skillName}`);
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
110
|
+
vmLog.warn("skill", `Failed to pull skill: ${message}`);
|
|
111
|
+
// Open Core fallback: if Cloud is unreachable, return any cached skill
|
|
112
|
+
const fallback = findAnyCachedSkill();
|
|
113
|
+
if (fallback) {
|
|
114
|
+
vmLog.dim("skill", `Using cached fallback: ${fallback.name} v${fallback.version}`);
|
|
115
|
+
return fallback;
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Scan the cache directory for any valid cached skill (Open Core fallback).
|
|
122
|
+
*/
|
|
123
|
+
function findAnyCachedSkill() {
|
|
124
|
+
if (!existsSync(CACHE_BASE))
|
|
125
|
+
return null;
|
|
126
|
+
const entries = readdirSync(CACHE_BASE, { withFileTypes: true });
|
|
127
|
+
for (const entry of entries) {
|
|
128
|
+
if (!entry.isDirectory())
|
|
129
|
+
continue;
|
|
130
|
+
const metadataPath = join(CACHE_BASE, entry.name, "skill.json");
|
|
131
|
+
if (!existsSync(metadataPath))
|
|
132
|
+
continue;
|
|
133
|
+
try {
|
|
134
|
+
const raw = readFileSync(metadataPath, "utf-8");
|
|
135
|
+
const parsed = JSON.parse(raw);
|
|
136
|
+
const result = SkillPullResponseSchema.safeParse(parsed);
|
|
137
|
+
if (result.success)
|
|
138
|
+
return result.data;
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
const AGENT_TYPE_DIRS = {
|
|
147
|
+
CLAUDE_CODE: ".claude",
|
|
148
|
+
OPENCODE: ".opencode",
|
|
149
|
+
};
|
|
150
|
+
/**
|
|
151
|
+
* Deposit skill files from cache into an agent's worktree.
|
|
152
|
+
*/
|
|
153
|
+
export function depositSkill(skill, worktreePath, agentType) {
|
|
154
|
+
const agentDir = AGENT_TYPE_DIRS[agentType] ?? ".claude";
|
|
155
|
+
const depositBase = join(worktreePath, agentDir, "skills", skill.name);
|
|
156
|
+
for (const file of skill.files) {
|
|
157
|
+
const safePath = normalize(file.path);
|
|
158
|
+
const resolved = join(depositBase, safePath);
|
|
159
|
+
if (!resolved.startsWith(depositBase + "/") && resolved !== depositBase) {
|
|
160
|
+
continue; // prevent path traversal
|
|
161
|
+
}
|
|
162
|
+
mkdirSync(dirname(resolved), { recursive: true });
|
|
163
|
+
writeFileSync(resolved, file.content, "utf-8");
|
|
164
|
+
}
|
|
165
|
+
vmLog.info("skill", `Deposited ${skill.name} (${skill.files.length} files) in ${depositBase}`);
|
|
166
|
+
}
|
|
167
|
+
//# sourceMappingURL=skill-manager.js.map
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { type Socket } from "socket.io-client";
|
|
2
|
+
import type { ClientToServerEvents, ServerToClientEvents, AgentStatusPayload, AgentStreamPayload, AgentToolActivityPayload, TaskUpdatePayload, CliHeartbeatPayload, TaskAskHumanEventPayload, WorkflowStepCompletePayload, WorkflowStreamPayload, WorkflowStreamEndPayload, WorkflowAgentIdlePayload, WorkflowAgentTimeoutPayload, WorkflowQuestionPayload, WorkflowToolActivityPayload, WorkflowTokenUsagePayload, ForgeCreateTaskGroupPayload, ForgeAddGroupDependencyPayload, ForgeCreateTaskPayload, ForgeAddTaskDependencyPayload, ForgeUpdateTaskPayload, ForgeDeleteTaskPayload, ForgeUpdateTaskGroupPayload, ForgeDeleteTaskGroupPayload, ForgeRemoveTaskDependencyPayload, ForgeRemoveGroupDependencyPayload } from "@getforgeai/protocol";
|
|
3
|
+
type TypedClientSocket = Socket<ServerToClientEvents, ClientToServerEvents>;
|
|
4
|
+
type McpAckError = {
|
|
5
|
+
error: string;
|
|
6
|
+
message?: string;
|
|
7
|
+
name?: string;
|
|
8
|
+
title?: string;
|
|
9
|
+
};
|
|
10
|
+
type TaskGroupCreateAck = {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
} | McpAckError;
|
|
14
|
+
type TaskGroupAddDependencyAck = {
|
|
15
|
+
success: true;
|
|
16
|
+
} | McpAckError;
|
|
17
|
+
type TaskCreateAck = {
|
|
18
|
+
id: string;
|
|
19
|
+
title: string;
|
|
20
|
+
} | McpAckError;
|
|
21
|
+
type TaskAddDependencyAck = {
|
|
22
|
+
success: true;
|
|
23
|
+
} | McpAckError;
|
|
24
|
+
type McpUpdateAck = {
|
|
25
|
+
success: true;
|
|
26
|
+
id: string;
|
|
27
|
+
} | McpAckError;
|
|
28
|
+
type McpDeleteAck = {
|
|
29
|
+
success: true;
|
|
30
|
+
id: string;
|
|
31
|
+
} | McpAckError;
|
|
32
|
+
type McpRemoveDependencyAck = {
|
|
33
|
+
success: true;
|
|
34
|
+
} | McpAckError;
|
|
35
|
+
export declare function createSocketClient(serverUrl: string, token: string, orgId: string): {
|
|
36
|
+
socket: TypedClientSocket;
|
|
37
|
+
emitAgentStatus: (payload: Omit<AgentStatusPayload, "orgId" | "timestamp">) => TypedClientSocket;
|
|
38
|
+
emitAgentStream: (payload: Omit<AgentStreamPayload, "orgId" | "timestamp">) => TypedClientSocket;
|
|
39
|
+
emitAgentToolActivity: (payload: Omit<AgentToolActivityPayload, "orgId" | "timestamp">) => TypedClientSocket;
|
|
40
|
+
emitTaskUpdate: (payload: Omit<TaskUpdatePayload, "orgId" | "timestamp">) => TypedClientSocket;
|
|
41
|
+
emitCliHeartbeat: (payload: Omit<CliHeartbeatPayload, "orgId" | "timestamp">) => TypedClientSocket;
|
|
42
|
+
emitTaskAskHuman: (payload: Omit<TaskAskHumanEventPayload, "orgId" | "timestamp">) => TypedClientSocket;
|
|
43
|
+
emitWorkflowStream: (payload: Omit<WorkflowStreamPayload, "orgId" | "timestamp">) => TypedClientSocket;
|
|
44
|
+
emitWorkflowStreamEnd: (payload: Omit<WorkflowStreamEndPayload, "orgId" | "timestamp">) => TypedClientSocket;
|
|
45
|
+
emitWorkflowQuestion: (payload: Omit<WorkflowQuestionPayload, "orgId" | "timestamp">) => TypedClientSocket;
|
|
46
|
+
emitWorkflowToolActivity: (payload: Omit<WorkflowToolActivityPayload, "orgId" | "timestamp">) => TypedClientSocket;
|
|
47
|
+
emitWorkflowTokenUsage: (payload: Omit<WorkflowTokenUsagePayload, "orgId" | "timestamp">) => TypedClientSocket;
|
|
48
|
+
emitStepComplete: (payload: Omit<WorkflowStepCompletePayload, "orgId" | "timestamp">) => TypedClientSocket;
|
|
49
|
+
emitWorkflowAgentIdle: (payload: Omit<WorkflowAgentIdlePayload, "orgId" | "timestamp">) => TypedClientSocket;
|
|
50
|
+
emitWorkflowAgentTimeout: (payload: Omit<WorkflowAgentTimeoutPayload, "orgId" | "timestamp">) => TypedClientSocket;
|
|
51
|
+
emitTaskGroupCreate: (payload: Omit<ForgeCreateTaskGroupPayload, "orgId" | "timestamp">) => Promise<TaskGroupCreateAck>;
|
|
52
|
+
emitTaskGroupAddDependency: (payload: Omit<ForgeAddGroupDependencyPayload, "orgId" | "timestamp">) => Promise<TaskGroupAddDependencyAck>;
|
|
53
|
+
emitTaskCreate: (payload: Omit<ForgeCreateTaskPayload, "orgId" | "timestamp">) => Promise<TaskCreateAck>;
|
|
54
|
+
emitTaskAddDependency: (payload: Omit<ForgeAddTaskDependencyPayload, "orgId" | "timestamp">) => Promise<TaskAddDependencyAck>;
|
|
55
|
+
emitMcpTaskUpdate: (payload: Omit<ForgeUpdateTaskPayload, "orgId" | "timestamp">) => Promise<McpUpdateAck>;
|
|
56
|
+
emitMcpTaskDelete: (payload: Omit<ForgeDeleteTaskPayload, "orgId" | "timestamp">) => Promise<McpDeleteAck>;
|
|
57
|
+
emitMcpTaskGroupUpdate: (payload: Omit<ForgeUpdateTaskGroupPayload, "orgId" | "timestamp">) => Promise<McpUpdateAck>;
|
|
58
|
+
emitMcpTaskGroupDelete: (payload: Omit<ForgeDeleteTaskGroupPayload, "orgId" | "timestamp">) => Promise<McpDeleteAck>;
|
|
59
|
+
emitMcpTaskRemoveDependency: (payload: Omit<ForgeRemoveTaskDependencyPayload, "orgId" | "timestamp">) => Promise<McpRemoveDependencyAck>;
|
|
60
|
+
emitMcpTaskGroupRemoveDependency: (payload: Omit<ForgeRemoveGroupDependencyPayload, "orgId" | "timestamp">) => Promise<McpRemoveDependencyAck>;
|
|
61
|
+
joinProject: (projectId: string) => TypedClientSocket;
|
|
62
|
+
leaveProject: (projectId: string) => TypedClientSocket;
|
|
63
|
+
joinMember: (memberId: string) => TypedClientSocket;
|
|
64
|
+
leaveMember: (memberId: string) => TypedClientSocket;
|
|
65
|
+
disconnect: () => TypedClientSocket;
|
|
66
|
+
};
|
|
67
|
+
export {};
|
|
68
|
+
//# sourceMappingURL=socket-client.d.ts.map
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { io } from "socket.io-client";
|
|
2
|
+
export function createSocketClient(serverUrl, token, orgId) {
|
|
3
|
+
const socket = io(`${serverUrl}/org:${orgId}`, {
|
|
4
|
+
auth: { token },
|
|
5
|
+
transports: ["websocket"],
|
|
6
|
+
reconnection: true,
|
|
7
|
+
reconnectionDelay: 1000,
|
|
8
|
+
reconnectionDelayMax: 30000,
|
|
9
|
+
reconnectionAttempts: Infinity,
|
|
10
|
+
});
|
|
11
|
+
return {
|
|
12
|
+
socket,
|
|
13
|
+
emitAgentStatus: (payload) => socket.emit("agent:status", {
|
|
14
|
+
...payload,
|
|
15
|
+
orgId,
|
|
16
|
+
timestamp: new Date().toISOString(),
|
|
17
|
+
}),
|
|
18
|
+
emitAgentStream: (payload) => socket.emit("agent:stream", {
|
|
19
|
+
...payload,
|
|
20
|
+
orgId,
|
|
21
|
+
timestamp: new Date().toISOString(),
|
|
22
|
+
}),
|
|
23
|
+
emitAgentToolActivity: (payload) => socket.emit("agent:tool-activity", {
|
|
24
|
+
...payload,
|
|
25
|
+
orgId,
|
|
26
|
+
timestamp: new Date().toISOString(),
|
|
27
|
+
}),
|
|
28
|
+
emitTaskUpdate: (payload) => socket.emit("task:update", {
|
|
29
|
+
...payload,
|
|
30
|
+
orgId,
|
|
31
|
+
timestamp: new Date().toISOString(),
|
|
32
|
+
}),
|
|
33
|
+
emitCliHeartbeat: (payload) => socket.emit("cli:heartbeat", {
|
|
34
|
+
...payload,
|
|
35
|
+
orgId,
|
|
36
|
+
timestamp: new Date().toISOString(),
|
|
37
|
+
}),
|
|
38
|
+
emitTaskAskHuman: (payload) => socket.emit("task:ask-human", {
|
|
39
|
+
...payload,
|
|
40
|
+
orgId,
|
|
41
|
+
timestamp: new Date().toISOString(),
|
|
42
|
+
}),
|
|
43
|
+
emitWorkflowStream: (payload) => socket.emit("workflow:stream", {
|
|
44
|
+
...payload,
|
|
45
|
+
orgId,
|
|
46
|
+
timestamp: new Date().toISOString(),
|
|
47
|
+
}),
|
|
48
|
+
emitWorkflowStreamEnd: (payload) => socket.emit("workflow:stream:end", {
|
|
49
|
+
...payload,
|
|
50
|
+
orgId,
|
|
51
|
+
timestamp: new Date().toISOString(),
|
|
52
|
+
}),
|
|
53
|
+
emitWorkflowQuestion: (payload) => socket.emit("workflow:question", {
|
|
54
|
+
...payload,
|
|
55
|
+
orgId,
|
|
56
|
+
timestamp: new Date().toISOString(),
|
|
57
|
+
}),
|
|
58
|
+
emitWorkflowToolActivity: (payload) => socket.emit("workflow:tool-activity", {
|
|
59
|
+
...payload,
|
|
60
|
+
orgId,
|
|
61
|
+
timestamp: new Date().toISOString(),
|
|
62
|
+
}),
|
|
63
|
+
emitWorkflowTokenUsage: (payload) => socket.emit("workflow:token-usage", {
|
|
64
|
+
...payload,
|
|
65
|
+
orgId,
|
|
66
|
+
timestamp: new Date().toISOString(),
|
|
67
|
+
}),
|
|
68
|
+
emitStepComplete: (payload) => socket.emit("workflow:step:complete", {
|
|
69
|
+
...payload,
|
|
70
|
+
orgId,
|
|
71
|
+
timestamp: new Date().toISOString(),
|
|
72
|
+
}),
|
|
73
|
+
emitWorkflowAgentIdle: (payload) => socket.emit("workflow:agent:idle", {
|
|
74
|
+
...payload,
|
|
75
|
+
orgId,
|
|
76
|
+
timestamp: new Date().toISOString(),
|
|
77
|
+
}),
|
|
78
|
+
emitWorkflowAgentTimeout: (payload) => socket.emit("workflow:agent:timeout", {
|
|
79
|
+
...payload,
|
|
80
|
+
orgId,
|
|
81
|
+
timestamp: new Date().toISOString(),
|
|
82
|
+
}),
|
|
83
|
+
emitTaskGroupCreate: (payload) => new Promise((resolve, reject) => {
|
|
84
|
+
const timer = setTimeout(() => reject(new Error("Socket ack timeout for taskGroup:mcp:create (30s)")), 30_000);
|
|
85
|
+
socket.emit("taskGroup:mcp:create", { ...payload, orgId, timestamp: new Date().toISOString() }, (ack) => {
|
|
86
|
+
clearTimeout(timer);
|
|
87
|
+
resolve(ack);
|
|
88
|
+
});
|
|
89
|
+
}),
|
|
90
|
+
emitTaskGroupAddDependency: (payload) => new Promise((resolve, reject) => {
|
|
91
|
+
const timer = setTimeout(() => reject(new Error("Socket ack timeout for taskGroup:mcp:add-dependency (30s)")), 30_000);
|
|
92
|
+
socket.emit("taskGroup:mcp:add-dependency", { ...payload, orgId, timestamp: new Date().toISOString() }, (ack) => {
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
resolve(ack);
|
|
95
|
+
});
|
|
96
|
+
}),
|
|
97
|
+
emitTaskCreate: (payload) => new Promise((resolve, reject) => {
|
|
98
|
+
const timer = setTimeout(() => reject(new Error("Socket ack timeout for task:mcp:create (30s)")), 30_000);
|
|
99
|
+
socket.emit("task:mcp:create", { ...payload, orgId, timestamp: new Date().toISOString() }, (ack) => {
|
|
100
|
+
clearTimeout(timer);
|
|
101
|
+
resolve(ack);
|
|
102
|
+
});
|
|
103
|
+
}),
|
|
104
|
+
emitTaskAddDependency: (payload) => new Promise((resolve, reject) => {
|
|
105
|
+
const timer = setTimeout(() => reject(new Error("Socket ack timeout for task:mcp:add-dependency (30s)")), 30_000);
|
|
106
|
+
socket.emit("task:mcp:add-dependency", { ...payload, orgId, timestamp: new Date().toISOString() }, (ack) => {
|
|
107
|
+
clearTimeout(timer);
|
|
108
|
+
resolve(ack);
|
|
109
|
+
});
|
|
110
|
+
}),
|
|
111
|
+
emitMcpTaskUpdate: (payload) => new Promise((resolve, reject) => {
|
|
112
|
+
const timer = setTimeout(() => reject(new Error("Socket ack timeout for task:mcp:update (30s)")), 30_000);
|
|
113
|
+
socket.emit("task:mcp:update", { ...payload, orgId, timestamp: new Date().toISOString() }, (ack) => {
|
|
114
|
+
clearTimeout(timer);
|
|
115
|
+
resolve(ack);
|
|
116
|
+
});
|
|
117
|
+
}),
|
|
118
|
+
emitMcpTaskDelete: (payload) => new Promise((resolve, reject) => {
|
|
119
|
+
const timer = setTimeout(() => reject(new Error("Socket ack timeout for task:mcp:delete (30s)")), 30_000);
|
|
120
|
+
socket.emit("task:mcp:delete", { ...payload, orgId, timestamp: new Date().toISOString() }, (ack) => {
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
resolve(ack);
|
|
123
|
+
});
|
|
124
|
+
}),
|
|
125
|
+
emitMcpTaskGroupUpdate: (payload) => new Promise((resolve, reject) => {
|
|
126
|
+
const timer = setTimeout(() => reject(new Error("Socket ack timeout for taskGroup:mcp:update (30s)")), 30_000);
|
|
127
|
+
socket.emit("taskGroup:mcp:update", { ...payload, orgId, timestamp: new Date().toISOString() }, (ack) => {
|
|
128
|
+
clearTimeout(timer);
|
|
129
|
+
resolve(ack);
|
|
130
|
+
});
|
|
131
|
+
}),
|
|
132
|
+
emitMcpTaskGroupDelete: (payload) => new Promise((resolve, reject) => {
|
|
133
|
+
const timer = setTimeout(() => reject(new Error("Socket ack timeout for taskGroup:mcp:delete (30s)")), 30_000);
|
|
134
|
+
socket.emit("taskGroup:mcp:delete", { ...payload, orgId, timestamp: new Date().toISOString() }, (ack) => {
|
|
135
|
+
clearTimeout(timer);
|
|
136
|
+
resolve(ack);
|
|
137
|
+
});
|
|
138
|
+
}),
|
|
139
|
+
emitMcpTaskRemoveDependency: (payload) => new Promise((resolve, reject) => {
|
|
140
|
+
const timer = setTimeout(() => reject(new Error("Socket ack timeout for task:mcp:remove-dependency (30s)")), 30_000);
|
|
141
|
+
socket.emit("task:mcp:remove-dependency", { ...payload, orgId, timestamp: new Date().toISOString() }, (ack) => {
|
|
142
|
+
clearTimeout(timer);
|
|
143
|
+
resolve(ack);
|
|
144
|
+
});
|
|
145
|
+
}),
|
|
146
|
+
emitMcpTaskGroupRemoveDependency: (payload) => new Promise((resolve, reject) => {
|
|
147
|
+
const timer = setTimeout(() => reject(new Error("Socket ack timeout for taskGroup:mcp:remove-dependency (30s)")), 30_000);
|
|
148
|
+
socket.emit("taskGroup:mcp:remove-dependency", { ...payload, orgId, timestamp: new Date().toISOString() }, (ack) => {
|
|
149
|
+
clearTimeout(timer);
|
|
150
|
+
resolve(ack);
|
|
151
|
+
});
|
|
152
|
+
}),
|
|
153
|
+
joinProject: (projectId) => socket.emit("join:project", projectId),
|
|
154
|
+
leaveProject: (projectId) => socket.emit("leave:project", projectId),
|
|
155
|
+
joinMember: (memberId) => socket.emit("join:member", memberId),
|
|
156
|
+
leaveMember: (memberId) => socket.emit("leave:member", memberId),
|
|
157
|
+
disconnect: () => socket.disconnect(),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=socket-client.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Probe the MCP server to verify it can start and respond to JSON-RPC initialize.
|
|
3
|
+
*
|
|
4
|
+
* Spawns `forgeai mcp --task-id probe --project-id probe --org-id probe`,
|
|
5
|
+
* sends a JSON-RPC initialize request, and verifies a response arrives
|
|
6
|
+
* within the timeout.
|
|
7
|
+
*
|
|
8
|
+
* Returns true if the MCP server is healthy, false otherwise.
|
|
9
|
+
*/
|
|
10
|
+
export declare function probeMcpHealth(): Promise<boolean>;
|
|
11
|
+
//# sourceMappingURL=mcp-probe.d.ts.map
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
const PROBE_TIMEOUT_MS = 10_000;
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the ForgeAI CLI binary for MCP probe.
|
|
6
|
+
* Same logic as agent-spawner's resolveForgeBinary.
|
|
7
|
+
*/
|
|
8
|
+
function resolveForgeBinary() {
|
|
9
|
+
const scriptPath = process.argv[1];
|
|
10
|
+
if (scriptPath) {
|
|
11
|
+
return { command: process.execPath, prefixArgs: [scriptPath] };
|
|
12
|
+
}
|
|
13
|
+
return { command: "forgeai", prefixArgs: [] };
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Probe the MCP server to verify it can start and respond to JSON-RPC initialize.
|
|
17
|
+
*
|
|
18
|
+
* Spawns `forgeai mcp --task-id probe --project-id probe --org-id probe`,
|
|
19
|
+
* sends a JSON-RPC initialize request, and verifies a response arrives
|
|
20
|
+
* within the timeout.
|
|
21
|
+
*
|
|
22
|
+
* Returns true if the MCP server is healthy, false otherwise.
|
|
23
|
+
*/
|
|
24
|
+
export async function probeMcpHealth() {
|
|
25
|
+
const { command, prefixArgs } = resolveForgeBinary();
|
|
26
|
+
const args = [
|
|
27
|
+
...prefixArgs,
|
|
28
|
+
"mcp",
|
|
29
|
+
"--task-id",
|
|
30
|
+
"probe",
|
|
31
|
+
"--project-id",
|
|
32
|
+
"probe",
|
|
33
|
+
"--org-id",
|
|
34
|
+
"probe",
|
|
35
|
+
];
|
|
36
|
+
return new Promise((resolve) => {
|
|
37
|
+
let settled = false;
|
|
38
|
+
let stdout = "";
|
|
39
|
+
const timer = setTimeout(() => {
|
|
40
|
+
if (settled)
|
|
41
|
+
return;
|
|
42
|
+
settled = true;
|
|
43
|
+
console.warn(chalk.yellow("[mcp-probe] Timeout — MCP server did not respond"));
|
|
44
|
+
child.kill("SIGTERM");
|
|
45
|
+
// Force kill if SIGTERM doesn't work after 2s
|
|
46
|
+
setTimeout(() => {
|
|
47
|
+
try {
|
|
48
|
+
child.kill("SIGKILL");
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Process already exited
|
|
52
|
+
}
|
|
53
|
+
}, 2_000);
|
|
54
|
+
resolve(false);
|
|
55
|
+
}, PROBE_TIMEOUT_MS);
|
|
56
|
+
const child = spawn(command, args, {
|
|
57
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
58
|
+
detached: false,
|
|
59
|
+
env: { ...process.env },
|
|
60
|
+
});
|
|
61
|
+
child.on("error", (err) => {
|
|
62
|
+
if (settled)
|
|
63
|
+
return;
|
|
64
|
+
settled = true;
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
console.warn(chalk.yellow(`[mcp-probe] Failed to spawn MCP process: ${err.message}`));
|
|
67
|
+
resolve(false);
|
|
68
|
+
});
|
|
69
|
+
child.on("exit", (code) => {
|
|
70
|
+
if (settled)
|
|
71
|
+
return;
|
|
72
|
+
settled = true;
|
|
73
|
+
clearTimeout(timer);
|
|
74
|
+
// If process exits before we get a response, it's unhealthy
|
|
75
|
+
console.warn(chalk.yellow(`[mcp-probe] MCP process exited with code ${code}`));
|
|
76
|
+
resolve(false);
|
|
77
|
+
});
|
|
78
|
+
child.stdout?.on("data", (chunk) => {
|
|
79
|
+
stdout += chunk.toString();
|
|
80
|
+
// Look for JSON-RPC response with "result" containing "serverInfo"
|
|
81
|
+
// The MCP server responds with: {"jsonrpc":"2.0","id":0,"result":{"protocolVersion":"...","serverInfo":{"name":"forgeai",...},...}}
|
|
82
|
+
if (stdout.includes('"serverInfo"') && stdout.includes('"result"')) {
|
|
83
|
+
if (settled)
|
|
84
|
+
return;
|
|
85
|
+
settled = true;
|
|
86
|
+
clearTimeout(timer);
|
|
87
|
+
console.log(chalk.green("[mcp-probe] MCP server is healthy"));
|
|
88
|
+
child.kill("SIGTERM");
|
|
89
|
+
resolve(true);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
// Send JSON-RPC initialize request
|
|
93
|
+
const initRequest = JSON.stringify({
|
|
94
|
+
jsonrpc: "2.0",
|
|
95
|
+
id: 0,
|
|
96
|
+
method: "initialize",
|
|
97
|
+
params: {
|
|
98
|
+
protocolVersion: "2024-11-05",
|
|
99
|
+
capabilities: {},
|
|
100
|
+
clientInfo: { name: "forgeai-probe", version: "1.0.0" },
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
child.stdin?.write(initRequest + "\n");
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=mcp-probe.js.map
|