@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.
Files changed (221) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +80 -0
  3. package/dist/bin/forgeai.d.ts +3 -0
  4. package/dist/bin/forgeai.js +8 -0
  5. package/dist/cli.d.ts +4 -0
  6. package/dist/cli.js +180 -0
  7. package/dist/commands/auth-login.d.ts +4 -0
  8. package/dist/commands/auth-login.js +202 -0
  9. package/dist/commands/auth-logout.d.ts +2 -0
  10. package/dist/commands/auth-logout.js +41 -0
  11. package/dist/commands/auth-status.d.ts +4 -0
  12. package/dist/commands/auth-status.js +81 -0
  13. package/dist/commands/cli-list.d.ts +4 -0
  14. package/dist/commands/cli-list.js +40 -0
  15. package/dist/commands/cli-unlink.d.ts +4 -0
  16. package/dist/commands/cli-unlink.js +45 -0
  17. package/dist/commands/config-set.d.ts +5 -0
  18. package/dist/commands/config-set.js +63 -0
  19. package/dist/commands/connect.d.ts +9 -0
  20. package/dist/commands/connect.js +165 -0
  21. package/dist/commands/disconnect.d.ts +5 -0
  22. package/dist/commands/disconnect.js +126 -0
  23. package/dist/commands/journal-clear.d.ts +2 -0
  24. package/dist/commands/journal-clear.js +16 -0
  25. package/dist/commands/journal-list.d.ts +2 -0
  26. package/dist/commands/journal-list.js +20 -0
  27. package/dist/commands/journal-status.d.ts +2 -0
  28. package/dist/commands/journal-status.js +18 -0
  29. package/dist/commands/mcp-serve.d.ts +13 -0
  30. package/dist/commands/mcp-serve.js +74 -0
  31. package/dist/commands/org-list.d.ts +4 -0
  32. package/dist/commands/org-list.js +56 -0
  33. package/dist/commands/org-unlink.d.ts +4 -0
  34. package/dist/commands/org-unlink.js +63 -0
  35. package/dist/commands/reconnect.d.ts +4 -0
  36. package/dist/commands/reconnect.js +46 -0
  37. package/dist/commands/release-open.d.ts +2 -0
  38. package/dist/commands/release-open.js +43 -0
  39. package/dist/commands/session-respond.d.ts +2 -0
  40. package/dist/commands/session-respond.js +29 -0
  41. package/dist/commands/task-open.d.ts +2 -0
  42. package/dist/commands/task-open.js +42 -0
  43. package/dist/commands/vm.d.ts +21 -0
  44. package/dist/commands/vm.js +122 -0
  45. package/dist/commands/workflow-open.d.ts +2 -0
  46. package/dist/commands/workflow-open.js +43 -0
  47. package/dist/config/config-manager.d.ts +31 -0
  48. package/dist/config/config-manager.js +70 -0
  49. package/dist/config/token-manager.d.ts +11 -0
  50. package/dist/config/token-manager.js +87 -0
  51. package/dist/index.d.ts +11 -0
  52. package/dist/index.js +22 -0
  53. package/dist/lib/auth-client.d.ts +1670 -0
  54. package/dist/lib/auth-client.js +10 -0
  55. package/dist/lib/branch-manager.d.ts +79 -0
  56. package/dist/lib/branch-manager.js +620 -0
  57. package/dist/lib/clone-and-checkout.d.ts +6 -0
  58. package/dist/lib/clone-and-checkout.js +41 -0
  59. package/dist/lib/connection-manager.d.ts +58 -0
  60. package/dist/lib/connection-manager.js +436 -0
  61. package/dist/lib/daemon-entry.d.ts +2 -0
  62. package/dist/lib/daemon-entry.js +210 -0
  63. package/dist/lib/daemon.d.ts +38 -0
  64. package/dist/lib/daemon.js +148 -0
  65. package/dist/lib/event-journal.d.ts +24 -0
  66. package/dist/lib/event-journal.js +76 -0
  67. package/dist/lib/ipc-client.d.ts +10 -0
  68. package/dist/lib/ipc-client.js +70 -0
  69. package/dist/lib/ipc-server.d.ts +36 -0
  70. package/dist/lib/ipc-server.js +70 -0
  71. package/dist/lib/node-version-guard.d.ts +8 -0
  72. package/dist/lib/node-version-guard.js +60 -0
  73. package/dist/lib/open-directory.d.ts +7 -0
  74. package/dist/lib/open-directory.js +41 -0
  75. package/dist/lib/repo-manager.d.ts +29 -0
  76. package/dist/lib/repo-manager.js +103 -0
  77. package/dist/lib/resilient-emitter.d.ts +53 -0
  78. package/dist/lib/resilient-emitter.js +160 -0
  79. package/dist/lib/rest-client.d.ts +164 -0
  80. package/dist/lib/rest-client.js +139 -0
  81. package/dist/lib/skill-manager.d.ts +23 -0
  82. package/dist/lib/skill-manager.js +167 -0
  83. package/dist/lib/socket-client.d.ts +68 -0
  84. package/dist/lib/socket-client.js +160 -0
  85. package/dist/mcp/mcp-probe.d.ts +11 -0
  86. package/dist/mcp/mcp-probe.js +106 -0
  87. package/dist/mcp/mcp-response.d.ts +29 -0
  88. package/dist/mcp/mcp-response.js +67 -0
  89. package/dist/mcp/mcp-server.d.ts +19 -0
  90. package/dist/mcp/mcp-server.js +76 -0
  91. package/dist/mcp/resources/project-conventions.d.ts +4 -0
  92. package/dist/mcp/resources/project-conventions.js +50 -0
  93. package/dist/mcp/resources/task-current.d.ts +4 -0
  94. package/dist/mcp/resources/task-current.js +51 -0
  95. package/dist/mcp/tools/forge-add-group-dependency.d.ts +4 -0
  96. package/dist/mcp/tools/forge-add-group-dependency.js +20 -0
  97. package/dist/mcp/tools/forge-add-task-dependency.d.ts +4 -0
  98. package/dist/mcp/tools/forge-add-task-dependency.js +20 -0
  99. package/dist/mcp/tools/forge-ask-human.d.ts +4 -0
  100. package/dist/mcp/tools/forge-ask-human.js +80 -0
  101. package/dist/mcp/tools/forge-column-done.d.ts +4 -0
  102. package/dist/mcp/tools/forge-column-done.js +46 -0
  103. package/dist/mcp/tools/forge-complete-step.d.ts +4 -0
  104. package/dist/mcp/tools/forge-complete-step.js +139 -0
  105. package/dist/mcp/tools/forge-create-task-group.d.ts +4 -0
  106. package/dist/mcp/tools/forge-create-task-group.js +22 -0
  107. package/dist/mcp/tools/forge-create-task.d.ts +4 -0
  108. package/dist/mcp/tools/forge-create-task.js +28 -0
  109. package/dist/mcp/tools/forge-delete-task-group.d.ts +4 -0
  110. package/dist/mcp/tools/forge-delete-task-group.js +17 -0
  111. package/dist/mcp/tools/forge-delete-task.d.ts +4 -0
  112. package/dist/mcp/tools/forge-delete-task.js +17 -0
  113. package/dist/mcp/tools/forge-get-task.d.ts +4 -0
  114. package/dist/mcp/tools/forge-get-task.js +17 -0
  115. package/dist/mcp/tools/forge-merge-back.d.ts +4 -0
  116. package/dist/mcp/tools/forge-merge-back.js +179 -0
  117. package/dist/mcp/tools/forge-release-merge.d.ts +4 -0
  118. package/dist/mcp/tools/forge-release-merge.js +159 -0
  119. package/dist/mcp/tools/forge-release-notes.d.ts +4 -0
  120. package/dist/mcp/tools/forge-release-notes.js +51 -0
  121. package/dist/mcp/tools/forge-release-tag.d.ts +4 -0
  122. package/dist/mcp/tools/forge-release-tag.js +125 -0
  123. package/dist/mcp/tools/forge-remove-group-dependency.d.ts +4 -0
  124. package/dist/mcp/tools/forge-remove-group-dependency.js +19 -0
  125. package/dist/mcp/tools/forge-remove-task-dependency.d.ts +4 -0
  126. package/dist/mcp/tools/forge-remove-task-dependency.js +19 -0
  127. package/dist/mcp/tools/forge-requeue.d.ts +4 -0
  128. package/dist/mcp/tools/forge-requeue.js +39 -0
  129. package/dist/mcp/tools/forge-submit-pr.d.ts +4 -0
  130. package/dist/mcp/tools/forge-submit-pr.js +68 -0
  131. package/dist/mcp/tools/forge-update-status.d.ts +4 -0
  132. package/dist/mcp/tools/forge-update-status.js +77 -0
  133. package/dist/mcp/tools/forge-update-task-group.d.ts +4 -0
  134. package/dist/mcp/tools/forge-update-task-group.js +21 -0
  135. package/dist/mcp/tools/forge-update-task.d.ts +4 -0
  136. package/dist/mcp/tools/forge-update-task.js +25 -0
  137. package/dist/mcp/tools/forge-validate-merge.d.ts +4 -0
  138. package/dist/mcp/tools/forge-validate-merge.js +194 -0
  139. package/dist/mcp/types.d.ts +39 -0
  140. package/dist/mcp/types.js +8 -0
  141. package/dist/methodology/artifact-manager.d.ts +14 -0
  142. package/dist/methodology/artifact-manager.js +55 -0
  143. package/dist/orchestrator/agent-registry.d.ts +53 -0
  144. package/dist/orchestrator/agent-registry.js +96 -0
  145. package/dist/orchestrator/agent-spawner.d.ts +112 -0
  146. package/dist/orchestrator/agent-spawner.js +1518 -0
  147. package/dist/orchestrator/connectors/claude-code-connector.d.ts +3 -0
  148. package/dist/orchestrator/connectors/claude-code-connector.js +60 -0
  149. package/dist/orchestrator/connectors/connector.d.ts +95 -0
  150. package/dist/orchestrator/connectors/connector.js +25 -0
  151. package/dist/orchestrator/connectors/docker-connector.d.ts +14 -0
  152. package/dist/orchestrator/connectors/docker-connector.js +337 -0
  153. package/dist/orchestrator/connectors/native-vm-connector.d.ts +11 -0
  154. package/dist/orchestrator/connectors/native-vm-connector.js +162 -0
  155. package/dist/orchestrator/connectors/opencode-connector.d.ts +3 -0
  156. package/dist/orchestrator/connectors/opencode-connector.js +39 -0
  157. package/dist/orchestrator/connectors/vm-connector.d.ts +3 -0
  158. package/dist/orchestrator/connectors/vm-connector.js +139 -0
  159. package/dist/orchestrator/dispatch-queue.d.ts +43 -0
  160. package/dist/orchestrator/dispatch-queue.js +93 -0
  161. package/dist/orchestrator/resume-handler.d.ts +34 -0
  162. package/dist/orchestrator/resume-handler.js +159 -0
  163. package/dist/orchestrator/stream-batcher.d.ts +16 -0
  164. package/dist/orchestrator/stream-batcher.js +50 -0
  165. package/dist/orchestrator/stream-json-extractor.d.ts +97 -0
  166. package/dist/orchestrator/stream-json-extractor.js +579 -0
  167. package/dist/vm/claude-token-manager.d.ts +36 -0
  168. package/dist/vm/claude-token-manager.js +223 -0
  169. package/dist/vm/docker-executor.d.ts +131 -0
  170. package/dist/vm/docker-executor.js +360 -0
  171. package/dist/vm/docker-process-proxy.d.ts +31 -0
  172. package/dist/vm/docker-process-proxy.js +105 -0
  173. package/dist/vm/forge-vm-agent.d.ts +11 -0
  174. package/dist/vm/forge-vm-agent.js +159 -0
  175. package/dist/vm/git-credentials.d.ts +43 -0
  176. package/dist/vm/git-credentials.js +53 -0
  177. package/dist/vm/image-manager.d.ts +7 -0
  178. package/dist/vm/image-manager.js +61 -0
  179. package/dist/vm/log.d.ts +9 -0
  180. package/dist/vm/log.js +18 -0
  181. package/dist/vm/mcp-bridge.d.ts +63 -0
  182. package/dist/vm/mcp-bridge.js +243 -0
  183. package/dist/vm/mcp-stub.d.ts +15 -0
  184. package/dist/vm/mcp-stub.js +67 -0
  185. package/dist/vm/native-addon.d.ts +69 -0
  186. package/dist/vm/native-addon.js +68 -0
  187. package/dist/vm/provider-resolver.d.ts +7 -0
  188. package/dist/vm/provider-resolver.js +27 -0
  189. package/dist/vm/providers/lima.d.ts +17 -0
  190. package/dist/vm/providers/lima.js +191 -0
  191. package/dist/vm/providers/qemu.d.ts +28 -0
  192. package/dist/vm/providers/qemu.js +260 -0
  193. package/dist/vm/providers/vfkit.d.ts +40 -0
  194. package/dist/vm/providers/vfkit.js +371 -0
  195. package/dist/vm/providers/wsl2.d.ts +27 -0
  196. package/dist/vm/providers/wsl2.js +226 -0
  197. package/dist/vm/session-manager.d.ts +130 -0
  198. package/dist/vm/session-manager.js +494 -0
  199. package/dist/vm/session-snapshot.d.ts +31 -0
  200. package/dist/vm/session-snapshot.js +180 -0
  201. package/dist/vm/types.d.ts +73 -0
  202. package/dist/vm/types.js +2 -0
  203. package/dist/vm/vm-helper-client.d.ts +81 -0
  204. package/dist/vm/vm-helper-client.js +335 -0
  205. package/dist/vm/vm-helper.d.ts +11 -0
  206. package/dist/vm/vm-helper.js +156 -0
  207. package/dist/vm/vm-ipc-protocol.d.ts +84 -0
  208. package/dist/vm/vm-ipc-protocol.js +9 -0
  209. package/dist/vm/vm-manager-facade.d.ts +23 -0
  210. package/dist/vm/vm-manager-facade.js +43 -0
  211. package/dist/vm/vm-manager.d.ts +74 -0
  212. package/dist/vm/vm-manager.js +515 -0
  213. package/dist/vm/vm-process-proxy.d.ts +34 -0
  214. package/dist/vm/vm-process-proxy.js +64 -0
  215. package/dist/vm/vsock-bridge.d.ts +44 -0
  216. package/dist/vm/vsock-bridge.js +140 -0
  217. package/dist/vm/vsock-protocol.d.ts +98 -0
  218. package/dist/vm/vsock-protocol.js +57 -0
  219. package/package.json +65 -0
  220. package/rootfs/Dockerfile +50 -0
  221. 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
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=daemon-entry.d.ts.map