@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,58 @@
|
|
|
1
|
+
import { createRestClient } from "./rest-client.js";
|
|
2
|
+
import { createSocketClient } from "./socket-client.js";
|
|
3
|
+
import { EventJournal } from "./event-journal.js";
|
|
4
|
+
import { ResilientEmitter } from "./resilient-emitter.js";
|
|
5
|
+
export type OrgConnection = {
|
|
6
|
+
orgId: string;
|
|
7
|
+
orgSlug: string;
|
|
8
|
+
socketClient: ReturnType<typeof createSocketClient>;
|
|
9
|
+
restClient: ReturnType<typeof createRestClient>;
|
|
10
|
+
journal: EventJournal;
|
|
11
|
+
emitter: ResilientEmitter;
|
|
12
|
+
heartbeatInterval: ReturnType<typeof setInterval>;
|
|
13
|
+
};
|
|
14
|
+
/** Active connections keyed by orgId. */
|
|
15
|
+
export declare const activeConnections: Map<string, OrgConnection>;
|
|
16
|
+
export type ConnectOptions = {
|
|
17
|
+
server?: string;
|
|
18
|
+
org?: string;
|
|
19
|
+
};
|
|
20
|
+
export type ResolvedConnectContext = {
|
|
21
|
+
token: string;
|
|
22
|
+
serverUrl: string;
|
|
23
|
+
cliId: string;
|
|
24
|
+
cliName: string;
|
|
25
|
+
agentType: string;
|
|
26
|
+
agentSlots: number;
|
|
27
|
+
targetOrgs: Array<{
|
|
28
|
+
orgId: string;
|
|
29
|
+
orgSlug: string;
|
|
30
|
+
}>;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Run MCP health probe and manage re-probe lifecycle.
|
|
34
|
+
* Called once after connections are established.
|
|
35
|
+
* If unhealthy, schedules periodic re-probes until healthy.
|
|
36
|
+
* Emits updated heartbeats to all active connections when health changes.
|
|
37
|
+
*/
|
|
38
|
+
export declare function runMcpProbe(ctx?: ResolvedConnectContext): Promise<void>;
|
|
39
|
+
/** Get current MCP health state (exported for agent-spawner error handler). */
|
|
40
|
+
export declare function getMcpHealthy(): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Resolve token, config, and target orgs. Returns null if validation fails
|
|
43
|
+
* (error messages are printed to console).
|
|
44
|
+
*/
|
|
45
|
+
export declare function resolveConnectContext(options: ConnectOptions): ResolvedConnectContext | null;
|
|
46
|
+
/**
|
|
47
|
+
* Establish connections for all target orgs.
|
|
48
|
+
* Populates the activeConnections map and sets up event handlers.
|
|
49
|
+
* When `append` is true, adds to existing connections without clearing them
|
|
50
|
+
* (used by daemon reconnect to re-establish server-disconnected orgs).
|
|
51
|
+
*/
|
|
52
|
+
export declare function establishConnections(ctx: ResolvedConnectContext, append?: boolean): void;
|
|
53
|
+
/**
|
|
54
|
+
* Gracefully shutdown all active connections.
|
|
55
|
+
* Syncs pending events (with timeout), disconnects sockets, clears intervals.
|
|
56
|
+
*/
|
|
57
|
+
export declare function shutdownConnections(): Promise<void>;
|
|
58
|
+
//# sourceMappingURL=connection-manager.d.ts.map
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { getConfig, getServerUrl, setConfig, } from "../config/config-manager.js";
|
|
4
|
+
import { loadToken, stableHostname } from "../config/token-manager.js";
|
|
5
|
+
import { createRestClient, RestClientError } from "./rest-client.js";
|
|
6
|
+
import { createSocketClient } from "./socket-client.js";
|
|
7
|
+
import { EventJournal } from "./event-journal.js";
|
|
8
|
+
import { ResilientEmitter } from "./resilient-emitter.js";
|
|
9
|
+
import { registerArtifactHandlers } from "../methodology/artifact-manager.js";
|
|
10
|
+
import { registerBranchHandlers } from "./branch-manager.js";
|
|
11
|
+
import { registerDispatchHandler, registerResumeHandler, registerWorkflowStepDispatchHandler, registerWorkflowAgentDismissHandler, stopAllAgents, getAgentRegistry, clearAllIdleWindows, } from "../orchestrator/agent-spawner.js";
|
|
12
|
+
import { dispatchQueue } from "../orchestrator/dispatch-queue.js";
|
|
13
|
+
import { probeMcpHealth } from "../mcp/mcp-probe.js";
|
|
14
|
+
const SYNC_SHUTDOWN_TIMEOUT_MS = 5_000;
|
|
15
|
+
const MCP_REPROBE_INTERVAL_MS = 60_000; // Re-probe every 60s when unhealthy
|
|
16
|
+
/** Module-level MCP health state shared across all heartbeats. */
|
|
17
|
+
let mcpHealthy = true; // Optimistic default until first probe completes
|
|
18
|
+
let mcpReprobeTimer = null;
|
|
19
|
+
let mcpProbeInFlight = false; // Prevent concurrent probe runs
|
|
20
|
+
/** Active connections keyed by orgId. */
|
|
21
|
+
export const activeConnections = new Map();
|
|
22
|
+
/**
|
|
23
|
+
* Run MCP health probe and manage re-probe lifecycle.
|
|
24
|
+
* Called once after connections are established.
|
|
25
|
+
* If unhealthy, schedules periodic re-probes until healthy.
|
|
26
|
+
* Emits updated heartbeats to all active connections when health changes.
|
|
27
|
+
*/
|
|
28
|
+
export async function runMcpProbe(ctx) {
|
|
29
|
+
if (mcpProbeInFlight)
|
|
30
|
+
return;
|
|
31
|
+
mcpProbeInFlight = true;
|
|
32
|
+
let healthy;
|
|
33
|
+
try {
|
|
34
|
+
healthy = await probeMcpHealth();
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
mcpProbeInFlight = false;
|
|
38
|
+
}
|
|
39
|
+
const changed = healthy !== mcpHealthy;
|
|
40
|
+
mcpHealthy = healthy;
|
|
41
|
+
if (changed) {
|
|
42
|
+
console.log(healthy
|
|
43
|
+
? chalk.green("[mcp-probe] MCP health restored — dispatches will resume")
|
|
44
|
+
: chalk.yellow("[mcp-probe] MCP unhealthy — this CLI will not receive dispatches"));
|
|
45
|
+
// Broadcast updated heartbeat immediately so Cloud sees the change
|
|
46
|
+
if (ctx) {
|
|
47
|
+
broadcastHealthHeartbeat(ctx);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Manage re-probe timer
|
|
51
|
+
if (!healthy && !mcpReprobeTimer) {
|
|
52
|
+
mcpReprobeTimer = setInterval(() => {
|
|
53
|
+
void runMcpProbe(ctx);
|
|
54
|
+
}, MCP_REPROBE_INTERVAL_MS);
|
|
55
|
+
}
|
|
56
|
+
else if (healthy && mcpReprobeTimer) {
|
|
57
|
+
clearInterval(mcpReprobeTimer);
|
|
58
|
+
mcpReprobeTimer = null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** Broadcast an immediate heartbeat to all active connections with current MCP health. */
|
|
62
|
+
function broadcastHealthHeartbeat(ctx) {
|
|
63
|
+
const reg = getAgentRegistry(ctx.agentSlots);
|
|
64
|
+
for (const conn of activeConnections.values()) {
|
|
65
|
+
conn.emitter.emitCliHeartbeat({
|
|
66
|
+
cliId: ctx.cliId,
|
|
67
|
+
status: "CONNECTED",
|
|
68
|
+
name: ctx.cliName,
|
|
69
|
+
agentSlots: ctx.agentSlots,
|
|
70
|
+
activeAgents: reg.getProvisionedCount(),
|
|
71
|
+
runningAgents: reg.getActiveCount(),
|
|
72
|
+
mcpHealthy,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Get current MCP health state (exported for agent-spawner error handler). */
|
|
77
|
+
export function getMcpHealthy() {
|
|
78
|
+
return mcpHealthy;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Resolve token, config, and target orgs. Returns null if validation fails
|
|
82
|
+
* (error messages are printed to console).
|
|
83
|
+
*/
|
|
84
|
+
export function resolveConnectContext(options) {
|
|
85
|
+
const token = loadToken();
|
|
86
|
+
if (!token) {
|
|
87
|
+
console.error(chalk.red("Not authenticated. Run `forge auth login` first."));
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
const config = getConfig();
|
|
91
|
+
const serverUrl = options.server ?? getServerUrl();
|
|
92
|
+
let targetOrgs;
|
|
93
|
+
if (config.linkedOrgs && config.linkedOrgs.length > 0) {
|
|
94
|
+
if (options.org) {
|
|
95
|
+
const match = config.linkedOrgs.find((o) => o.orgSlug === options.org);
|
|
96
|
+
if (!match) {
|
|
97
|
+
console.error(chalk.red(`Organization "${options.org}" not found in linked orgs. Available: ${config.linkedOrgs.map((o) => o.orgSlug).join(", ")}`));
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
targetOrgs = [match];
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
targetOrgs = config.linkedOrgs;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else if (config.defaultOrgId) {
|
|
107
|
+
targetOrgs = [
|
|
108
|
+
{
|
|
109
|
+
orgId: config.defaultOrgId,
|
|
110
|
+
orgSlug: config.defaultOrg ?? "default",
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
console.error(chalk.red("No organization configured. Run `forge auth login` to select one."));
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
const cliId = config.cliId ??
|
|
119
|
+
(() => {
|
|
120
|
+
const newId = randomUUID();
|
|
121
|
+
setConfig({ cliId: newId });
|
|
122
|
+
return newId;
|
|
123
|
+
})();
|
|
124
|
+
const cliName = config.cliName ?? stableHostname();
|
|
125
|
+
const agentType = config.agentType ?? "CLAUDE_CODE";
|
|
126
|
+
const agentSlots = config.agentSlots ?? 5;
|
|
127
|
+
return {
|
|
128
|
+
token,
|
|
129
|
+
serverUrl,
|
|
130
|
+
cliId,
|
|
131
|
+
cliName,
|
|
132
|
+
agentType,
|
|
133
|
+
agentSlots,
|
|
134
|
+
targetOrgs,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Establish connections for all target orgs.
|
|
139
|
+
* Populates the activeConnections map and sets up event handlers.
|
|
140
|
+
* When `append` is true, adds to existing connections without clearing them
|
|
141
|
+
* (used by daemon reconnect to re-establish server-disconnected orgs).
|
|
142
|
+
*/
|
|
143
|
+
export function establishConnections(ctx, append = false) {
|
|
144
|
+
const config = getConfig();
|
|
145
|
+
if (!append) {
|
|
146
|
+
activeConnections.clear();
|
|
147
|
+
}
|
|
148
|
+
// Skip orgs that already have an active connection
|
|
149
|
+
const orgsToConnect = append
|
|
150
|
+
? ctx.targetOrgs.filter((o) => !activeConnections.has(o.orgId))
|
|
151
|
+
: ctx.targetOrgs;
|
|
152
|
+
const totalOrgs = activeConnections.size + orgsToConnect.length;
|
|
153
|
+
const multiOrg = totalOrgs > 1;
|
|
154
|
+
if (orgsToConnect.length > 1) {
|
|
155
|
+
console.log(chalk.green(`Connecting to ${orgsToConnect.length} organizations: ${orgsToConnect.map((o) => o.orgSlug).join(", ")}`));
|
|
156
|
+
}
|
|
157
|
+
const prefix = (slug) => (multiOrg ? `[${slug}] ` : "");
|
|
158
|
+
let authFailures = 0;
|
|
159
|
+
for (const org of orgsToConnect) {
|
|
160
|
+
const client = createSocketClient(ctx.serverUrl, ctx.token, org.orgId);
|
|
161
|
+
const restClient = createRestClient(ctx.serverUrl, ctx.token, org.orgId);
|
|
162
|
+
const journal = new EventJournal(org.orgId);
|
|
163
|
+
let lastSyncCursor = config.lastSyncCursor ?? "";
|
|
164
|
+
const emitter = new ResilientEmitter({
|
|
165
|
+
socketClient: client,
|
|
166
|
+
restClient,
|
|
167
|
+
journal,
|
|
168
|
+
cliId: ctx.cliId,
|
|
169
|
+
orgId: org.orgId,
|
|
170
|
+
initialCursor: lastSyncCursor,
|
|
171
|
+
onSyncStart: () => {
|
|
172
|
+
const pending = journal.pendingCount();
|
|
173
|
+
if (pending > 0) {
|
|
174
|
+
console.log(chalk.green(`${prefix(org.orgSlug)}Reconnected -- syncing ${pending} pending events`));
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
onSyncComplete: (count) => {
|
|
178
|
+
console.log(chalk.green(`${prefix(org.orgSlug)}Synced ${count} events to Cloud`));
|
|
179
|
+
lastSyncCursor = emitter.getLastSyncCursor();
|
|
180
|
+
setConfig({
|
|
181
|
+
lastSyncCursor,
|
|
182
|
+
lastSyncTimestamp: new Date().toISOString(),
|
|
183
|
+
});
|
|
184
|
+
},
|
|
185
|
+
onSyncError: (error) => {
|
|
186
|
+
console.warn(chalk.yellow(`${prefix(org.orgSlug)}Sync failed: ${error.message}`));
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
// Register methodology artifact handlers (Story 4.4)
|
|
190
|
+
registerArtifactHandlers(client.socket, org.orgId);
|
|
191
|
+
// Register branch handlers (Story 5c.1)
|
|
192
|
+
registerBranchHandlers(client.socket, org.orgId);
|
|
193
|
+
// Register agent dispatch handler (Story 6.3, queue added Story 6.5)
|
|
194
|
+
const drainCtx = registerDispatchHandler(client.socket, org.orgId, {
|
|
195
|
+
restClient,
|
|
196
|
+
socketClient: client,
|
|
197
|
+
agentType: ctx.agentType,
|
|
198
|
+
agentSlots: ctx.agentSlots,
|
|
199
|
+
queue: dispatchQueue,
|
|
200
|
+
});
|
|
201
|
+
// Register agent resume handler (Story 6.4, queue added Story 6.5)
|
|
202
|
+
registerResumeHandler(client.socket, org.orgId, {
|
|
203
|
+
restClient,
|
|
204
|
+
socketClient: client,
|
|
205
|
+
agentType: ctx.agentType,
|
|
206
|
+
agentSlots: ctx.agentSlots,
|
|
207
|
+
queue: dispatchQueue,
|
|
208
|
+
});
|
|
209
|
+
// Register workflow step dispatch handler (Story 6b-7)
|
|
210
|
+
registerWorkflowStepDispatchHandler(client.socket, org.orgId, {
|
|
211
|
+
restClient,
|
|
212
|
+
socketClient: client,
|
|
213
|
+
agentType: ctx.agentType,
|
|
214
|
+
agentSlots: ctx.agentSlots,
|
|
215
|
+
queue: dispatchQueue,
|
|
216
|
+
});
|
|
217
|
+
// Register workflow agent dismiss handler (Story 6b-10)
|
|
218
|
+
registerWorkflowAgentDismissHandler(client.socket, {
|
|
219
|
+
restClient,
|
|
220
|
+
socketClient: client,
|
|
221
|
+
agentType: ctx.agentType,
|
|
222
|
+
agentSlots: ctx.agentSlots,
|
|
223
|
+
queue: dispatchQueue,
|
|
224
|
+
});
|
|
225
|
+
// Listen for server-pushed config updates (e.g., admin changed agentSlots in UI)
|
|
226
|
+
client.socket.on("cli:config:updated", (payload) => {
|
|
227
|
+
if (payload.agentSlots != null && payload.agentSlots !== ctx.agentSlots) {
|
|
228
|
+
const previous = ctx.agentSlots;
|
|
229
|
+
ctx.agentSlots = payload.agentSlots;
|
|
230
|
+
// Update registry capacity
|
|
231
|
+
const reg = getAgentRegistry(previous);
|
|
232
|
+
reg.updateMaxSlots(payload.agentSlots);
|
|
233
|
+
// Persist to local config
|
|
234
|
+
setConfig({ agentSlots: payload.agentSlots });
|
|
235
|
+
console.log(chalk.green(`${prefix(org.orgSlug)}Agent slots updated by server: ${previous} → ${payload.agentSlots}`));
|
|
236
|
+
// Emit immediate heartbeat so server sees the new value without waiting 30s
|
|
237
|
+
emitter.emitCliHeartbeat({
|
|
238
|
+
cliId: ctx.cliId,
|
|
239
|
+
status: "CONNECTED",
|
|
240
|
+
name: ctx.cliName,
|
|
241
|
+
agentSlots: payload.agentSlots,
|
|
242
|
+
activeAgents: reg.getProvisionedCount(),
|
|
243
|
+
runningAgents: reg.getActiveCount(),
|
|
244
|
+
mcpHealthy,
|
|
245
|
+
});
|
|
246
|
+
// Drain queued dispatches now that capacity increased
|
|
247
|
+
if (payload.agentSlots > previous && dispatchQueue.size() > 0) {
|
|
248
|
+
void dispatchQueue.drainNext(drainCtx).catch((err) => {
|
|
249
|
+
console.warn(chalk.yellow(`[dispatch-queue] Drain after slot increase failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
client.socket.on("connect", async () => {
|
|
255
|
+
authFailures = 0;
|
|
256
|
+
console.log(chalk.green(`${prefix(org.orgSlug)}Connected to ForgeAI Cloud`));
|
|
257
|
+
const connectReg = getAgentRegistry(ctx.agentSlots);
|
|
258
|
+
emitter.emitCliHeartbeat({
|
|
259
|
+
cliId: ctx.cliId,
|
|
260
|
+
status: "CONNECTED",
|
|
261
|
+
name: ctx.cliName,
|
|
262
|
+
agentSlots: ctx.agentSlots,
|
|
263
|
+
activeAgents: connectReg.getProvisionedCount(),
|
|
264
|
+
runningAgents: connectReg.getActiveCount(),
|
|
265
|
+
mcpHealthy,
|
|
266
|
+
});
|
|
267
|
+
try {
|
|
268
|
+
const device = await restClient.registerCli({
|
|
269
|
+
cliId: ctx.cliId,
|
|
270
|
+
name: ctx.cliName,
|
|
271
|
+
agentType: ctx.agentType,
|
|
272
|
+
agentSlots: ctx.agentSlots,
|
|
273
|
+
});
|
|
274
|
+
console.log(chalk.green(`${prefix(org.orgSlug)}CLI registered as "${ctx.cliName}"`));
|
|
275
|
+
// Join member room so we receive workflow:step:dispatch events
|
|
276
|
+
if (device.memberId) {
|
|
277
|
+
client.socket.emit("join:member", device.memberId);
|
|
278
|
+
console.log(chalk.dim(`${prefix(org.orgSlug)}Joined member room: ${device.memberId}`));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
let message = error instanceof Error ? error.message : String(error);
|
|
283
|
+
try {
|
|
284
|
+
const jsonMatch = /\{[\s\S]*\}/.exec(message);
|
|
285
|
+
if (jsonMatch) {
|
|
286
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
287
|
+
if (parsed.message) {
|
|
288
|
+
message = parsed.message;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
// Fall back to raw message
|
|
294
|
+
}
|
|
295
|
+
const isLimitError = message.includes("plan limit") ||
|
|
296
|
+
message.includes("CLI limit") ||
|
|
297
|
+
(error instanceof RestClientError && error.status === 403);
|
|
298
|
+
if (isLimitError) {
|
|
299
|
+
console.warn(chalk.yellow(`\n${prefix(org.orgSlug)}${message}`));
|
|
300
|
+
console.warn(chalk.yellow("Upgrade your plan or unlink an existing CLI with: forge org unlink <slug>"));
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
console.warn(chalk.yellow(`${prefix(org.orgSlug)}CLI registration warning: ${message}`));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
client.socket.on("disconnect", (reason) => {
|
|
308
|
+
console.log(chalk.yellow(`${prefix(org.orgSlug)}Disconnected: ${reason} -- events will be journaled locally`));
|
|
309
|
+
// Clear dispatch queue on disconnect to prevent stale dispatches (Story 6.5)
|
|
310
|
+
dispatchQueue.clear();
|
|
311
|
+
if (reason === "io server disconnect") {
|
|
312
|
+
// Permanent disconnect: clean up idle windows (timers + registry slots)
|
|
313
|
+
clearAllIdleWindows(client.socket, ctx.agentSlots);
|
|
314
|
+
// Clean up heartbeat interval and connection for this org
|
|
315
|
+
const conn = activeConnections.get(org.orgId);
|
|
316
|
+
if (conn) {
|
|
317
|
+
clearInterval(conn.heartbeatInterval);
|
|
318
|
+
activeConnections.delete(org.orgId);
|
|
319
|
+
}
|
|
320
|
+
// Flush pending events via REST before fully releasing
|
|
321
|
+
const pending = emitter.getPendingCount();
|
|
322
|
+
if (pending > 0) {
|
|
323
|
+
console.log(chalk.cyan(`${prefix(org.orgSlug)}Flushing ${pending} pending events...`));
|
|
324
|
+
emitter.syncPendingEvents().then(() => console.log(chalk.green(`${prefix(org.orgSlug)}Events flushed`)), () => console.warn(chalk.yellow(`${prefix(org.orgSlug)}Flush failed -- events saved locally`)));
|
|
325
|
+
}
|
|
326
|
+
if (activeConnections.size === 0) {
|
|
327
|
+
console.log(chalk.cyan("All connections closed by server. Run `forge connect` to reconnect."));
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
client.socket.on("connect_error", (err) => {
|
|
332
|
+
console.error(chalk.red(`${prefix(org.orgSlug)}Connection error: ${err.message}`));
|
|
333
|
+
const isAuthError = err.message.includes("Authentication") ||
|
|
334
|
+
err.message.includes("Invalid session") ||
|
|
335
|
+
err.message.includes("Authentication required");
|
|
336
|
+
if (isAuthError) {
|
|
337
|
+
authFailures++;
|
|
338
|
+
if (authFailures >= 3) {
|
|
339
|
+
console.error(chalk.red("\nAuthentication failed repeatedly. Run `forge auth login` to re-authenticate."));
|
|
340
|
+
for (const conn of activeConnections.values()) {
|
|
341
|
+
conn.socketClient.disconnect();
|
|
342
|
+
}
|
|
343
|
+
process.exit(1);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
const heartbeatInterval = setInterval(() => {
|
|
348
|
+
const reg = getAgentRegistry(ctx.agentSlots);
|
|
349
|
+
emitter.emitCliHeartbeat({
|
|
350
|
+
cliId: ctx.cliId,
|
|
351
|
+
status: "CONNECTED",
|
|
352
|
+
name: ctx.cliName,
|
|
353
|
+
agentSlots: ctx.agentSlots,
|
|
354
|
+
activeAgents: reg.getProvisionedCount(),
|
|
355
|
+
runningAgents: reg.getActiveCount(),
|
|
356
|
+
mcpHealthy,
|
|
357
|
+
});
|
|
358
|
+
}, 30_000);
|
|
359
|
+
activeConnections.set(org.orgId, {
|
|
360
|
+
orgId: org.orgId,
|
|
361
|
+
orgSlug: org.orgSlug,
|
|
362
|
+
socketClient: client,
|
|
363
|
+
restClient,
|
|
364
|
+
journal,
|
|
365
|
+
emitter,
|
|
366
|
+
heartbeatInterval,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Gracefully shutdown all active connections.
|
|
372
|
+
* Syncs pending events (with timeout), disconnects sockets, clears intervals.
|
|
373
|
+
*/
|
|
374
|
+
export async function shutdownConnections() {
|
|
375
|
+
// Clear MCP re-probe timer
|
|
376
|
+
if (mcpReprobeTimer) {
|
|
377
|
+
clearInterval(mcpReprobeTimer);
|
|
378
|
+
mcpReprobeTimer = null;
|
|
379
|
+
}
|
|
380
|
+
// Clear dispatch queue and stop all running agents before disconnecting sockets (Story 6.3 + 6.5)
|
|
381
|
+
dispatchQueue.clear();
|
|
382
|
+
const config = getConfig();
|
|
383
|
+
for (const conn of activeConnections.values()) {
|
|
384
|
+
try {
|
|
385
|
+
await stopAllAgents({
|
|
386
|
+
restClient: conn.restClient,
|
|
387
|
+
socketClient: conn.socketClient,
|
|
388
|
+
agentType: config.agentType ?? "CLAUDE_CODE",
|
|
389
|
+
agentSlots: config.agentSlots ?? 5,
|
|
390
|
+
queue: dispatchQueue,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
// Best-effort agent shutdown
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
// Wait for agent exit handler REST calls (fallback advance/requeue) to complete
|
|
398
|
+
try {
|
|
399
|
+
const { awaitPendingFallbacks } = await import("../orchestrator/agent-spawner.js");
|
|
400
|
+
await awaitPendingFallbacks(5_000);
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
// Best-effort — agent-spawner may not be loaded
|
|
404
|
+
}
|
|
405
|
+
const multiOrg = activeConnections.size > 1;
|
|
406
|
+
const prefix = (slug) => (multiOrg ? `[${slug}] ` : "");
|
|
407
|
+
for (const conn of activeConnections.values()) {
|
|
408
|
+
clearInterval(conn.heartbeatInterval);
|
|
409
|
+
const pending = conn.emitter.getPendingCount();
|
|
410
|
+
if (pending > 0) {
|
|
411
|
+
console.log(chalk.cyan(`${prefix(conn.orgSlug)}Syncing ${pending} pending events before shutdown...`));
|
|
412
|
+
try {
|
|
413
|
+
await Promise.race([
|
|
414
|
+
conn.emitter.syncPendingEvents(),
|
|
415
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("Sync timeout")), SYNC_SHUTDOWN_TIMEOUT_MS)),
|
|
416
|
+
]);
|
|
417
|
+
console.log(chalk.green(`${prefix(conn.orgSlug)}Final sync complete`));
|
|
418
|
+
}
|
|
419
|
+
catch {
|
|
420
|
+
console.warn(chalk.yellow(`${prefix(conn.orgSlug)}Final sync timed out -- events will be synced on next connect`));
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
conn.socketClient.disconnect();
|
|
424
|
+
}
|
|
425
|
+
activeConnections.clear();
|
|
426
|
+
// Shutdown VM session manager (graceful VM stop)
|
|
427
|
+
try {
|
|
428
|
+
const { getVmManager } = await import("../vm/vm-manager-facade.js");
|
|
429
|
+
const sm = getVmManager();
|
|
430
|
+
await sm.shutdown();
|
|
431
|
+
}
|
|
432
|
+
catch {
|
|
433
|
+
// Session manager may not be initialized
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
//# sourceMappingURL=connection-manager.js.map
|