@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,210 @@
1
+ /**
2
+ * Daemon entry point — spawned as a detached child process by `spawnDaemon()`.
3
+ * Bootstraps the IPC server and establishes connections to all linked orgs.
4
+ */
5
+ import chalk from "chalk";
6
+ import { registerDaemonCleanup, DAEMON_PATHS } from "./daemon.js";
7
+ import { createIpcServer, } from "./ipc-server.js";
8
+ import { resolveConnectContext, establishConnections, activeConnections, shutdownConnections, runMcpProbe, } from "./connection-manager.js";
9
+ import { isVmCapable, initVmManager } from "../vm/vm-manager-facade.js";
10
+ // Register cleanup handlers first
11
+ registerDaemonCleanup();
12
+ console.log(`[daemon] Starting (PID: ${process.pid})`);
13
+ // Resolve connection context (mutable — reconnect handler may update it)
14
+ let ctx = resolveConnectContext({});
15
+ if (!ctx) {
16
+ console.error("[daemon] Failed to resolve connection context — exiting");
17
+ process.exit(1);
18
+ }
19
+ // Check Claude OAuth token availability
20
+ try {
21
+ const { loadClaudeToken } = await import("../vm/claude-token-manager.js");
22
+ if (!(await loadClaudeToken())) {
23
+ console.warn(chalk.yellow("[daemon] No Claude OAuth token stored — agents will fail to authenticate."));
24
+ console.warn(chalk.yellow("[daemon] Run: forge auth setup-claude"));
25
+ }
26
+ }
27
+ catch {
28
+ // Non-fatal
29
+ }
30
+ // Initialize VM manager (helper process or direct addon)
31
+ if (isVmCapable()) {
32
+ try {
33
+ await initVmManager();
34
+ console.log(chalk.green("[daemon] VM manager initialized"));
35
+ }
36
+ catch (err) {
37
+ console.warn(chalk.yellow(`[daemon] VM manager failed: ${err instanceof Error ? err.message : String(err)}`));
38
+ }
39
+ }
40
+ else {
41
+ console.warn(chalk.yellow("[daemon] No VM capability — agent execution disabled"));
42
+ }
43
+ // Establish connections to all target orgs
44
+ establishConnections(ctx);
45
+ // Run MCP health probe (non-blocking — heartbeats update when result arrives)
46
+ void runMcpProbe(ctx);
47
+ console.log(`[daemon] Connected to ${ctx.targetOrgs.length} org(s): ${ctx.targetOrgs.map((o) => o.orgSlug).join(", ")}`);
48
+ // Helper functions for IPC commands
49
+ async function handleStatusCommand() {
50
+ const orgs = Array.from(activeConnections.values()).map((conn) => ({
51
+ slug: conn.orgSlug,
52
+ connected: conn.socketClient.socket.connected,
53
+ }));
54
+ return { ok: true, orgs };
55
+ }
56
+ function reconnectDisconnectedSockets(orgFilter) {
57
+ const conns = orgFilter
58
+ ? Array.from(activeConnections.values()).filter((c) => c.orgSlug === orgFilter)
59
+ : Array.from(activeConnections.values());
60
+ for (const conn of conns) {
61
+ if (!conn.socketClient.socket.connected) {
62
+ conn.socketClient.socket.connect();
63
+ }
64
+ }
65
+ }
66
+ async function handleReconnectCommand(cmd) {
67
+ // Re-read config from disk to pick up org/token changes (e.g., after re-login)
68
+ const freshCtx = resolveConnectContext({});
69
+ if (!freshCtx) {
70
+ return {
71
+ ok: false,
72
+ error: "Failed to resolve connection context (not authenticated?)",
73
+ };
74
+ }
75
+ const tokenChanged = freshCtx.token !== ctx?.token;
76
+ ctx = freshCtx;
77
+ const freshOrgIds = new Set(ctx.targetOrgs.map((o) => o.orgId));
78
+ // Remove stale connections: orgs no longer in config, or ALL if token changed
79
+ for (const [orgId, conn] of activeConnections) {
80
+ if (tokenChanged || !freshOrgIds.has(orgId)) {
81
+ clearInterval(conn.heartbeatInterval);
82
+ conn.socketClient.disconnect();
83
+ activeConnections.delete(orgId);
84
+ console.log(`[daemon] Removed stale org: ${conn.orgSlug}`);
85
+ }
86
+ }
87
+ // Reconnect existing connections that lost their socket
88
+ if (!tokenChanged) {
89
+ reconnectDisconnectedSockets(cmd.org);
90
+ }
91
+ // Establish connections for new/missing orgs
92
+ const newOrgs = ctx.targetOrgs.filter((o) => !activeConnections.has(o.orgId));
93
+ if (newOrgs.length > 0) {
94
+ establishConnections({ ...ctx, targetOrgs: newOrgs }, true);
95
+ console.log(`[daemon] Added org(s): ${newOrgs.map((o) => o.orgSlug).join(", ")}`);
96
+ }
97
+ // Build response
98
+ const allConns = cmd.org
99
+ ? Array.from(activeConnections.values()).filter((c) => c.orgSlug === cmd.org)
100
+ : Array.from(activeConnections.values());
101
+ const orgs = allConns.map((conn) => ({
102
+ slug: conn.orgSlug,
103
+ connected: conn.socketClient.socket.connected,
104
+ }));
105
+ const status = newOrgs.length === 0 && orgs.every((o) => o.connected)
106
+ ? "already_connected"
107
+ : "reconnecting";
108
+ return {
109
+ ok: true,
110
+ status,
111
+ orgs,
112
+ };
113
+ }
114
+ async function handleDisconnectCommand(cmd) {
115
+ const targetConns = cmd.org
116
+ ? Array.from(activeConnections.values()).filter((c) => c.orgSlug === cmd.org)
117
+ : Array.from(activeConnections.values());
118
+ if (targetConns.length === 0) {
119
+ return {
120
+ ok: false,
121
+ error: cmd.org
122
+ ? `Organization "${cmd.org}" not found in active connections`
123
+ : "No active connections",
124
+ };
125
+ }
126
+ for (const conn of targetConns) {
127
+ // REST disconnect call to notify Cloud
128
+ try {
129
+ const config = (await import("../config/config-manager.js")).getConfig();
130
+ if (config.cliId) {
131
+ await conn.restClient.disconnectCli({ cliId: config.cliId });
132
+ }
133
+ }
134
+ catch {
135
+ // Best-effort REST disconnect
136
+ }
137
+ // Close WebSocket — daemon stays alive
138
+ conn.socketClient.disconnect();
139
+ }
140
+ const orgs = targetConns.map((conn) => ({
141
+ slug: conn.orgSlug,
142
+ connected: false,
143
+ }));
144
+ return { ok: true, status: "disconnected", orgs };
145
+ }
146
+ async function handleReRegisterCommand() {
147
+ // Re-read config and re-register with Cloud using updated values
148
+ const freshCtxReg = resolveConnectContext({});
149
+ if (!freshCtxReg) {
150
+ return {
151
+ ok: false,
152
+ error: "Failed to resolve connection context",
153
+ };
154
+ }
155
+ ctx = freshCtxReg;
156
+ // Re-register on all active connections using PATCH to explicitly update config
157
+ // (PATCH includes agentSlots from the CLI config to sync with Cloud)
158
+ for (const conn of activeConnections.values()) {
159
+ try {
160
+ await conn.restClient.updateCliConfig({
161
+ cliId: ctx.cliId,
162
+ name: ctx.cliName,
163
+ agentType: ctx.agentType,
164
+ agentSlots: ctx.agentSlots,
165
+ });
166
+ }
167
+ catch {
168
+ // Best-effort re-registration
169
+ }
170
+ }
171
+ return { ok: true, status: "re-registered" };
172
+ }
173
+ async function handleShutdownCommand() {
174
+ console.log("[daemon] Shutdown requested via IPC");
175
+ // Graceful shutdown: sync pending events, disconnect, clean up
176
+ await shutdownConnections();
177
+ // Close IPC server
178
+ ipcServer.close();
179
+ // Remove PID/socket files (cleanup handler will also run on exit)
180
+ try {
181
+ const fs = await import("node:fs");
182
+ if (fs.existsSync(DAEMON_PATHS.pid))
183
+ fs.unlinkSync(DAEMON_PATHS.pid);
184
+ if (fs.existsSync(DAEMON_PATHS.sock))
185
+ fs.unlinkSync(DAEMON_PATHS.sock);
186
+ }
187
+ catch {
188
+ // Best-effort
189
+ }
190
+ // Respond before exiting
191
+ setTimeout(() => process.exit(0), 100);
192
+ return { ok: true, status: "shutting_down" };
193
+ }
194
+ // Start IPC server
195
+ const ipcServer = createIpcServer(async (cmd) => {
196
+ switch (cmd.cmd) {
197
+ case "status":
198
+ return handleStatusCommand();
199
+ case "reconnect":
200
+ return handleReconnectCommand(cmd);
201
+ case "disconnect":
202
+ return handleDisconnectCommand(cmd);
203
+ case "re-register":
204
+ return handleReRegisterCommand();
205
+ case "shutdown":
206
+ return handleShutdownCommand();
207
+ }
208
+ });
209
+ console.log(`[daemon] IPC server listening on ${DAEMON_PATHS.sock}`);
210
+ //# sourceMappingURL=daemon-entry.js.map
@@ -0,0 +1,38 @@
1
+ export declare const DAEMON_PATHS: {
2
+ readonly dir: string;
3
+ readonly pid: string;
4
+ readonly sock: string;
5
+ readonly log: string;
6
+ };
7
+ /**
8
+ * Read the stored daemon PID from the PID file.
9
+ * Returns null if no PID file exists or it's unreadable.
10
+ */
11
+ export declare function readDaemonPid(): number | null;
12
+ /**
13
+ * Check if a process with the given PID is currently alive.
14
+ * Uses signal 0 which checks existence without sending a signal.
15
+ */
16
+ export declare function isProcessAlive(pid: number): boolean;
17
+ /**
18
+ * Check if the daemon is currently running.
19
+ * Returns the PID if running, null if not.
20
+ */
21
+ export declare function isDaemonRunning(): number | null;
22
+ /**
23
+ * Remove stale PID file and socket file when the daemon process
24
+ * no longer exists.
25
+ */
26
+ export declare function cleanStalePidFile(): void;
27
+ /**
28
+ * Spawn the daemon as a detached background process.
29
+ * Returns the child PID.
30
+ */
31
+ export declare function spawnDaemon(): number;
32
+ /**
33
+ * Register cleanup handlers on the current process to ensure
34
+ * PID and socket files are removed on exit.
35
+ * Should be called from the daemon entry point.
36
+ */
37
+ export declare function registerDaemonCleanup(): void;
38
+ //# sourceMappingURL=daemon.d.ts.map
@@ -0,0 +1,148 @@
1
+ import { existsSync, readFileSync, writeFileSync, unlinkSync, openSync, } from "node:fs";
2
+ import { spawn } from "node:child_process";
3
+ import { homedir } from "node:os";
4
+ import { join, resolve } from "node:path";
5
+ const FORGEAI_DIR = join(homedir(), ".forgeai");
6
+ export const DAEMON_PATHS = {
7
+ dir: FORGEAI_DIR,
8
+ pid: join(FORGEAI_DIR, "daemon.pid"),
9
+ sock: join(FORGEAI_DIR, "daemon.sock"),
10
+ log: join(FORGEAI_DIR, "daemon.log"),
11
+ };
12
+ /**
13
+ * Read the stored daemon PID from the PID file.
14
+ * Returns null if no PID file exists or it's unreadable.
15
+ */
16
+ export function readDaemonPid() {
17
+ try {
18
+ if (!existsSync(DAEMON_PATHS.pid))
19
+ return null;
20
+ const content = readFileSync(DAEMON_PATHS.pid, "utf-8").trim();
21
+ const pid = Number.parseInt(content, 10);
22
+ return Number.isNaN(pid) ? null : pid;
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ /**
29
+ * Check if a process with the given PID is currently alive.
30
+ * Uses signal 0 which checks existence without sending a signal.
31
+ */
32
+ export function isProcessAlive(pid) {
33
+ try {
34
+ process.kill(pid, 0);
35
+ return true;
36
+ }
37
+ catch (error) {
38
+ const code = error.code;
39
+ // ESRCH = no such process (stale)
40
+ // EPERM = process exists but no permission (alive)
41
+ if (code === "EPERM")
42
+ return true;
43
+ return false;
44
+ }
45
+ }
46
+ /**
47
+ * Check if the daemon is currently running.
48
+ * Returns the PID if running, null if not.
49
+ */
50
+ export function isDaemonRunning() {
51
+ const pid = readDaemonPid();
52
+ if (pid === null)
53
+ return null;
54
+ if (isProcessAlive(pid)) {
55
+ return pid;
56
+ }
57
+ // PID exists but process is dead — clean up stale files
58
+ cleanStalePidFile();
59
+ return null;
60
+ }
61
+ /**
62
+ * Remove stale PID file and socket file when the daemon process
63
+ * no longer exists.
64
+ */
65
+ export function cleanStalePidFile() {
66
+ try {
67
+ if (existsSync(DAEMON_PATHS.pid)) {
68
+ unlinkSync(DAEMON_PATHS.pid);
69
+ }
70
+ }
71
+ catch {
72
+ // Ignore cleanup errors
73
+ }
74
+ try {
75
+ if (existsSync(DAEMON_PATHS.sock)) {
76
+ unlinkSync(DAEMON_PATHS.sock);
77
+ }
78
+ }
79
+ catch {
80
+ // Ignore cleanup errors
81
+ }
82
+ }
83
+ /**
84
+ * Spawn the daemon as a detached background process.
85
+ * Returns the child PID.
86
+ */
87
+ export function spawnDaemon() {
88
+ // Resolve daemon entry point relative to this file's location
89
+ // In compiled output: dist/lib/daemon.ts → dist/lib/daemon-entry.js
90
+ const daemonEntryPath = resolve(import.meta.dirname, "daemon-entry.js");
91
+ // Open log file in append mode for daemon stdout/stderr
92
+ const logFd = openSync(DAEMON_PATHS.log, "a");
93
+ const child = spawn(process.execPath, [daemonEntryPath], {
94
+ detached: true,
95
+ stdio: ["ignore", logFd, logFd],
96
+ env: { ...process.env, FORGEAI_DAEMON: "1" },
97
+ });
98
+ child.unref();
99
+ const pid = child.pid;
100
+ if (pid === undefined) {
101
+ throw new Error("Failed to spawn daemon: no PID returned");
102
+ }
103
+ writeFileSync(DAEMON_PATHS.pid, String(pid), "utf-8");
104
+ return pid;
105
+ }
106
+ /**
107
+ * Register cleanup handlers on the current process to ensure
108
+ * PID and socket files are removed on exit.
109
+ * Should be called from the daemon entry point.
110
+ */
111
+ export function registerDaemonCleanup() {
112
+ const cleanup = () => {
113
+ try {
114
+ if (existsSync(DAEMON_PATHS.pid))
115
+ unlinkSync(DAEMON_PATHS.pid);
116
+ }
117
+ catch {
118
+ // Best-effort cleanup
119
+ }
120
+ try {
121
+ if (existsSync(DAEMON_PATHS.sock))
122
+ unlinkSync(DAEMON_PATHS.sock);
123
+ }
124
+ catch {
125
+ // Best-effort cleanup
126
+ }
127
+ };
128
+ process.on("exit", cleanup);
129
+ process.on("SIGINT", () => {
130
+ cleanup();
131
+ process.exit(0);
132
+ });
133
+ process.on("SIGTERM", () => {
134
+ cleanup();
135
+ process.exit(0);
136
+ });
137
+ process.on("uncaughtException", (err) => {
138
+ console.error("Daemon uncaught exception:", err);
139
+ cleanup();
140
+ process.exit(1);
141
+ });
142
+ process.on("unhandledRejection", (reason) => {
143
+ // Log but don't exit — unhandled rejections are often recoverable
144
+ // (e.g., socket emit on disconnected socket, async spawn errors)
145
+ console.error("Daemon unhandled rejection:", reason instanceof Error ? reason : String(reason));
146
+ });
147
+ }
148
+ //# sourceMappingURL=daemon.js.map
@@ -0,0 +1,24 @@
1
+ export type JournalEntry = {
2
+ eventId: string;
3
+ type: string;
4
+ timestamp: string;
5
+ cliId: string;
6
+ orgId: string;
7
+ projectId: string | null;
8
+ payload: Record<string, unknown>;
9
+ synced: boolean;
10
+ };
11
+ export type JournalAppendInput = Omit<JournalEntry, "eventId" | "synced">;
12
+ export declare class EventJournal {
13
+ private readonly filePath;
14
+ constructor(projectId: string);
15
+ private ensureDir;
16
+ append(event: JournalAppendInput): JournalEntry;
17
+ readAll(): JournalEntry[];
18
+ readSince(cursor: string): JournalEntry[];
19
+ getCursor(): string;
20
+ clear(upToCursor: string): void;
21
+ pendingCount(): number;
22
+ lastEventTimestamp(): string | null;
23
+ }
24
+ //# sourceMappingURL=event-journal.d.ts.map
@@ -0,0 +1,76 @@
1
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync, } from "node:fs";
2
+ import { randomUUID } from "node:crypto";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ const EVENTS_DIR = join(homedir(), ".forgeai", "events");
6
+ export class EventJournal {
7
+ filePath;
8
+ constructor(projectId) {
9
+ this.filePath = join(EVENTS_DIR, `${projectId}.ndjson`);
10
+ this.ensureDir();
11
+ }
12
+ ensureDir() {
13
+ if (!existsSync(EVENTS_DIR)) {
14
+ mkdirSync(EVENTS_DIR, { recursive: true, mode: 0o700 });
15
+ }
16
+ }
17
+ append(event) {
18
+ const entry = {
19
+ ...event,
20
+ eventId: randomUUID(),
21
+ synced: false,
22
+ };
23
+ appendFileSync(this.filePath, JSON.stringify(entry) + "\n", "utf-8");
24
+ return entry;
25
+ }
26
+ readAll() {
27
+ if (!existsSync(this.filePath))
28
+ return [];
29
+ const content = readFileSync(this.filePath, "utf-8");
30
+ const lines = content.split("\n").filter(Boolean);
31
+ const entries = [];
32
+ for (const line of lines) {
33
+ try {
34
+ entries.push(JSON.parse(line));
35
+ }
36
+ catch {
37
+ // Skip corrupted lines gracefully
38
+ }
39
+ }
40
+ return entries;
41
+ }
42
+ readSince(cursor) {
43
+ const entries = this.readAll();
44
+ if (!cursor)
45
+ return entries;
46
+ const cursorIndex = entries.findIndex((e) => e.eventId === cursor);
47
+ if (cursorIndex === -1)
48
+ return entries;
49
+ return entries.slice(cursorIndex + 1);
50
+ }
51
+ getCursor() {
52
+ const entries = this.readAll();
53
+ if (entries.length === 0)
54
+ return "";
55
+ return entries.at(-1).eventId;
56
+ }
57
+ clear(upToCursor) {
58
+ const entries = this.readAll();
59
+ const cursorIndex = entries.findIndex((e) => e.eventId === upToCursor);
60
+ if (cursorIndex === -1)
61
+ return;
62
+ const remaining = entries.slice(cursorIndex + 1);
63
+ const content = remaining.map((e) => JSON.stringify(e)).join("\n");
64
+ writeFileSync(this.filePath, content ? content + "\n" : "", "utf-8");
65
+ }
66
+ pendingCount() {
67
+ return this.readAll().length;
68
+ }
69
+ lastEventTimestamp() {
70
+ const entries = this.readAll();
71
+ if (entries.length === 0)
72
+ return null;
73
+ return entries.at(-1).timestamp;
74
+ }
75
+ }
76
+ //# sourceMappingURL=event-journal.js.map
@@ -0,0 +1,10 @@
1
+ import type { IpcCommand, IpcResponse } from "./ipc-server.js";
2
+ /**
3
+ * Send a command to the daemon via IPC (Unix domain socket).
4
+ * Returns the parsed response or throws on connection/timeout errors.
5
+ */
6
+ export declare function sendIpcCommand(cmd: IpcCommand, timeoutMs?: number): Promise<IpcResponse>;
7
+ export declare class DaemonNotRunningError extends Error {
8
+ constructor();
9
+ }
10
+ //# sourceMappingURL=ipc-client.d.ts.map
@@ -0,0 +1,70 @@
1
+ import { createConnection } from "node:net";
2
+ import { DAEMON_PATHS } from "./daemon.js";
3
+ const DEFAULT_TIMEOUT_MS = 5_000;
4
+ /**
5
+ * Send a command to the daemon via IPC (Unix domain socket).
6
+ * Returns the parsed response or throws on connection/timeout errors.
7
+ */
8
+ export async function sendIpcCommand(cmd, timeoutMs = DEFAULT_TIMEOUT_MS) {
9
+ return new Promise((resolve, reject) => {
10
+ const socket = createConnection(DAEMON_PATHS.sock);
11
+ let buffer = "";
12
+ let settled = false;
13
+ const timeout = setTimeout(() => {
14
+ if (!settled) {
15
+ settled = true;
16
+ socket.destroy();
17
+ reject(new Error("IPC command timed out"));
18
+ }
19
+ }, timeoutMs);
20
+ socket.on("connect", () => {
21
+ socket.write(JSON.stringify(cmd) + "\n");
22
+ });
23
+ socket.on("data", (chunk) => {
24
+ buffer += chunk.toString("utf8");
25
+ const lines = buffer.split("\n");
26
+ buffer = lines.pop() ?? "";
27
+ for (const line of lines) {
28
+ if (!line.trim())
29
+ continue;
30
+ if (!settled) {
31
+ settled = true;
32
+ clearTimeout(timeout);
33
+ socket.destroy();
34
+ try {
35
+ resolve(JSON.parse(line));
36
+ }
37
+ catch (err) {
38
+ reject(new Error(`Invalid IPC response: ${err instanceof Error ? err.message : String(err)}`));
39
+ }
40
+ }
41
+ }
42
+ });
43
+ socket.on("error", (err) => {
44
+ if (!settled) {
45
+ settled = true;
46
+ clearTimeout(timeout);
47
+ if (err.code === "ECONNREFUSED" || err.code === "ENOENT") {
48
+ reject(new DaemonNotRunningError());
49
+ }
50
+ else {
51
+ reject(new Error(`IPC connection error: ${err.message}`));
52
+ }
53
+ }
54
+ });
55
+ socket.on("close", () => {
56
+ if (!settled) {
57
+ settled = true;
58
+ clearTimeout(timeout);
59
+ reject(new Error("IPC connection closed before response"));
60
+ }
61
+ });
62
+ });
63
+ }
64
+ export class DaemonNotRunningError extends Error {
65
+ constructor() {
66
+ super("No daemon running. Run `forge connect` first.");
67
+ this.name = "DaemonNotRunningError";
68
+ }
69
+ }
70
+ //# sourceMappingURL=ipc-client.js.map
@@ -0,0 +1,36 @@
1
+ import { type Server } from "node:net";
2
+ export type IpcCommand = {
3
+ cmd: "status";
4
+ } | {
5
+ cmd: "reconnect";
6
+ org?: string;
7
+ } | {
8
+ cmd: "disconnect";
9
+ org?: string;
10
+ } | {
11
+ cmd: "re-register";
12
+ } | {
13
+ cmd: "shutdown";
14
+ };
15
+ export type OrgStatus = {
16
+ slug: string;
17
+ connected: boolean;
18
+ };
19
+ export type IpcResponse = {
20
+ ok: true;
21
+ orgs: OrgStatus[];
22
+ } | {
23
+ ok: true;
24
+ status: string;
25
+ orgs?: OrgStatus[];
26
+ } | {
27
+ ok: false;
28
+ error: string;
29
+ };
30
+ export type IpcCommandHandler = (cmd: IpcCommand) => Promise<IpcResponse>;
31
+ /**
32
+ * Create and start the IPC server on a Unix domain socket.
33
+ * Uses JSON Lines protocol (one JSON object per line, delimited by \n).
34
+ */
35
+ export declare function createIpcServer(handler: IpcCommandHandler): Server;
36
+ //# sourceMappingURL=ipc-server.d.ts.map
@@ -0,0 +1,70 @@
1
+ import { createServer } from "node:net";
2
+ import { existsSync, unlinkSync } from "node:fs";
3
+ import { DAEMON_PATHS } from "./daemon.js";
4
+ /**
5
+ * Create and start the IPC server on a Unix domain socket.
6
+ * Uses JSON Lines protocol (one JSON object per line, delimited by \n).
7
+ */
8
+ export function createIpcServer(handler) {
9
+ const server = createServer((socket) => {
10
+ let buffer = "";
11
+ socket.on("data", (chunk) => {
12
+ buffer += chunk.toString("utf8");
13
+ const lines = buffer.split("\n");
14
+ buffer = lines.pop() ?? ""; // keep incomplete trailing line
15
+ for (const line of lines) {
16
+ if (!line.trim())
17
+ continue;
18
+ void (async () => {
19
+ try {
20
+ const cmd = JSON.parse(line);
21
+ const response = await handler(cmd);
22
+ const responseLine = JSON.stringify(response) + "\n";
23
+ if (!socket.destroyed) {
24
+ socket.write(responseLine);
25
+ }
26
+ }
27
+ catch (err) {
28
+ const errorResponse = {
29
+ ok: false,
30
+ error: err instanceof Error ? err.message : String(err),
31
+ };
32
+ if (!socket.destroyed) {
33
+ socket.write(JSON.stringify(errorResponse) + "\n");
34
+ }
35
+ }
36
+ })();
37
+ }
38
+ });
39
+ socket.on("error", () => {
40
+ // Prevent ECONNRESET/EPIPE crashes — client disconnected unexpectedly
41
+ });
42
+ });
43
+ server.on("error", (err) => {
44
+ if (err.code === "EADDRINUSE") {
45
+ // Stale socket file — remove and retry
46
+ try {
47
+ unlinkSync(DAEMON_PATHS.sock);
48
+ }
49
+ catch {
50
+ // Ignore
51
+ }
52
+ server.listen(DAEMON_PATHS.sock);
53
+ }
54
+ else {
55
+ console.error("IPC server error:", err);
56
+ }
57
+ });
58
+ // Remove stale socket file before listening
59
+ if (existsSync(DAEMON_PATHS.sock)) {
60
+ try {
61
+ unlinkSync(DAEMON_PATHS.sock);
62
+ }
63
+ catch {
64
+ // Ignore
65
+ }
66
+ }
67
+ server.listen(DAEMON_PATHS.sock);
68
+ return server;
69
+ }
70
+ //# sourceMappingURL=ipc-server.js.map
@@ -0,0 +1,8 @@
1
+ /** True when `current` is at least `required`. Exported for tests. */
2
+ export declare function isSupportedNodeVersion(current: string, required: string): boolean;
3
+ /**
4
+ * Print an actionable message and exit(1) when Node is too old.
5
+ * Called once from the CLI entry point, before anything else runs.
6
+ */
7
+ export declare function assertSupportedNodeVersion(current?: string, required?: string): void;
8
+ //# sourceMappingURL=node-version-guard.d.ts.map