@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,180 @@
1
+ /**
2
+ * Session snapshot: save and restore Claude Code session files for workflow resume.
3
+ *
4
+ * Before destroying a container after idle timeout:
5
+ * 1. Auto-save code: git add -A && git commit && git push
6
+ * 2. Tar Claude Code session files (~/.claude/projects/)
7
+ * 3. Upload encrypted snapshot to Cloud via socket event
8
+ *
9
+ * On resume in a new container:
10
+ * 1. Download snapshot from Cloud
11
+ * 2. Restore Claude Code session files
12
+ * 3. Launch `claude --continue` (full context restored)
13
+ */
14
+ import { readFileSync, existsSync } from "node:fs";
15
+ import { join } from "node:path";
16
+ import { execInContainer } from "./docker-executor.js";
17
+ import { gitCredentialConfigShellFragment, gitCredentialSecretEnv, } from "./git-credentials.js";
18
+ import { vmLog } from "./log.js";
19
+ /** Max snapshot size: 5MB compressed */
20
+ const MAX_SNAPSHOT_SIZE = 5 * 1024 * 1024;
21
+ /**
22
+ * Auto-save code changes in the container (best-effort git commit + push).
23
+ * Does not throw — failures are logged and ignored.
24
+ */
25
+ export async function autoSaveCode(containerId, sessionId, credentials) {
26
+ // Network pushes need the credential re-supplied out-of-band: the repo was
27
+ // cloned with a clean URL, so nothing is embedded in .git/config. The helper
28
+ // reads the value from FORGE_GIT_* env vars provided via secretEnv.
29
+ const gitPush = credentials
30
+ ? `git ${gitCredentialConfigShellFragment()} push`
31
+ : "git push";
32
+ const pushExecOpts = credentials
33
+ ? { timeout: 30_000, secretEnv: gitCredentialSecretEnv(credentials) }
34
+ : { timeout: 30_000 };
35
+ try {
36
+ // 0. Log current branch and remote
37
+ const branchInfo = await execInContainer(containerId, [
38
+ "bash",
39
+ "-c",
40
+ "cd /session/workspace && echo \"branch=$(git rev-parse --abbrev-ref HEAD) remote=$(git remote get-url origin 2>/dev/null | sed 's/\\/\\/.*@/\\/\\//') \"",
41
+ ], { timeout: 5_000 });
42
+ vmLog.dim("session-snapshot", branchInfo.stdout.trim(), sessionId);
43
+ // 1. Stage all changes
44
+ await execInContainer(containerId, ["bash", "-c", "cd /session/workspace && git add -A"], { timeout: 10_000 });
45
+ // 2. Check if there's anything to commit
46
+ const diffResult = await execInContainer(containerId, ["bash", "-c", "cd /session/workspace && git diff --cached --quiet"], { timeout: 10_000 });
47
+ if (diffResult.exitCode === 0) {
48
+ vmLog.dim("session-snapshot", "No changes to commit", sessionId);
49
+ // Still try to push (agent may have committed but not pushed)
50
+ const pushResult = await execInContainer(containerId, ["bash", "-c", `cd /session/workspace && ${gitPush} 2>&1`], pushExecOpts);
51
+ if (pushResult.exitCode === 0) {
52
+ const output = pushResult.stdout.trim();
53
+ if (output.includes("Everything up-to-date")) {
54
+ vmLog.dim("session-snapshot", "Nothing to push (up-to-date)", sessionId);
55
+ }
56
+ else {
57
+ vmLog.cyan("session-snapshot", `Pushed existing commits: ${output.slice(0, 100)}`, sessionId);
58
+ }
59
+ }
60
+ else {
61
+ vmLog.warn("session-snapshot", `Push failed: ${pushResult.stderr.slice(0, 200)}`, sessionId);
62
+ }
63
+ return;
64
+ }
65
+ // 3. Commit staged changes
66
+ const commitResult = await execInContainer(containerId, [
67
+ "bash",
68
+ "-c",
69
+ 'cd /session/workspace && git commit -m "wip: auto-save step progress" 2>&1',
70
+ ], { timeout: 10_000 });
71
+ if (commitResult.exitCode !== 0) {
72
+ vmLog.warn("session-snapshot", `Commit failed: ${commitResult.stdout.slice(0, 200)}`, sessionId);
73
+ return;
74
+ }
75
+ vmLog.cyan("session-snapshot", "Committed changes", sessionId);
76
+ // 4. Push
77
+ const pushResult = await execInContainer(containerId, ["bash", "-c", `cd /session/workspace && ${gitPush} 2>&1`], pushExecOpts);
78
+ if (pushResult.exitCode !== 0) {
79
+ vmLog.error("session-snapshot", `Push FAILED: ${pushResult.stderr.slice(0, 200)} ${pushResult.stdout.slice(0, 200)}`, sessionId);
80
+ }
81
+ else {
82
+ vmLog.cyan("session-snapshot", "Auto-saved code (commit + push)", sessionId);
83
+ }
84
+ }
85
+ catch (err) {
86
+ vmLog.warn("session-snapshot", `Auto-save failed: ${err instanceof Error ? err.message : String(err)}`, sessionId);
87
+ }
88
+ }
89
+ /**
90
+ * Save Claude Code session files from the container to a base64 tar.gz string.
91
+ *
92
+ * Returns the base64-encoded tar.gz, or null if snapshot is unavailable or too large.
93
+ */
94
+ export async function createSessionSnapshot(session) {
95
+ const sessionId = session.id;
96
+ try {
97
+ // Tar the entire Claude Code config directory (~/.claude/).
98
+ // --continue needs: history.jsonl (global session index) + projects/<encoded-path>/ (session JSONL files)
99
+ // Exclude settings.json and other non-session files to keep size small.
100
+ const tarResult = await execInContainer(session.containerId, [
101
+ "bash",
102
+ "-c",
103
+ [
104
+ "test -d /home/agent/.claude || exit 0",
105
+ "tar czf /session/claude-session.tar.gz -C /home/agent/.claude" +
106
+ " --exclude='settings.json'" +
107
+ " --exclude='credentials.json'" +
108
+ " --exclude='statsig'" +
109
+ " --exclude='commands'" +
110
+ " --exclude='todos'" +
111
+ " .",
112
+ ].join(" && "),
113
+ ], { timeout: 30_000 });
114
+ if (tarResult.exitCode !== 0) {
115
+ vmLog.warn("session-snapshot", `Tar failed (code ${tarResult.exitCode}): ${tarResult.stderr.slice(0, 200)}`, sessionId);
116
+ return null;
117
+ }
118
+ // Read the tar from the host volume
119
+ const tarPath = join(session.sessionDir, "claude-session.tar.gz");
120
+ if (!existsSync(tarPath)) {
121
+ vmLog.dim("session-snapshot", "No Claude session directory found in container, skipping snapshot", sessionId);
122
+ return null;
123
+ }
124
+ const tarBuffer = readFileSync(tarPath);
125
+ // Check size limit
126
+ if (tarBuffer.length > MAX_SNAPSHOT_SIZE) {
127
+ vmLog.warn("session-snapshot", `Snapshot too large (${(tarBuffer.length / 1024 / 1024).toFixed(1)}MB > 5MB), skipping`, sessionId);
128
+ return null;
129
+ }
130
+ vmLog.cyan("session-snapshot", `Created snapshot (${(tarBuffer.length / 1024).toFixed(0)}KB compressed)`, sessionId);
131
+ return tarBuffer.toString("base64");
132
+ }
133
+ catch (err) {
134
+ vmLog.warn("session-snapshot", `Snapshot creation failed: ${err instanceof Error ? err.message : String(err)}`, sessionId);
135
+ return null;
136
+ }
137
+ }
138
+ /**
139
+ * Restore Claude Code session files into a container from a base64 tar.gz string.
140
+ */
141
+ export async function restoreSessionSnapshot(session, snapshotBase64) {
142
+ const sessionId = session.id;
143
+ try {
144
+ // Write the tar to the host volume
145
+ const tarPath = join(session.sessionDir, "claude-session.tar.gz");
146
+ const { writeFileSync } = await import("node:fs");
147
+ writeFileSync(tarPath, Buffer.from(snapshotBase64, "base64"));
148
+ // Create the target directory and extract (entire ~/.claude/)
149
+ await execInContainer(session.containerId, [
150
+ "mkdir",
151
+ "-p",
152
+ "/home/agent/.claude",
153
+ ]);
154
+ const extractResult = await execInContainer(session.containerId, [
155
+ "tar",
156
+ "xzf",
157
+ "/session/claude-session.tar.gz",
158
+ "-C",
159
+ "/home/agent/.claude",
160
+ ], { timeout: 30_000 });
161
+ if (extractResult.exitCode !== 0) {
162
+ vmLog.warn("session-snapshot", `Tar extract failed (code ${extractResult.exitCode}): ${extractResult.stderr.slice(0, 200)}`, sessionId);
163
+ return false;
164
+ }
165
+ // Fix ownership (tar may have preserved root ownership from the build)
166
+ await execInContainer(session.containerId, [
167
+ "chown",
168
+ "-R",
169
+ "agent:agent",
170
+ "/home/agent/.claude",
171
+ ]);
172
+ vmLog.cyan("session-snapshot", "Restored Claude session snapshot", sessionId);
173
+ return true;
174
+ }
175
+ catch (err) {
176
+ vmLog.warn("session-snapshot", `Snapshot restore failed: ${err instanceof Error ? err.message : String(err)}`, sessionId);
177
+ return false;
178
+ }
179
+ }
180
+ //# sourceMappingURL=session-snapshot.js.map
@@ -0,0 +1,73 @@
1
+ import type { ChildProcess } from "node:child_process";
2
+ export type NetworkRule = {
3
+ cidr: string;
4
+ port: number;
5
+ protocol: "tcp" | "udp";
6
+ };
7
+ export type VmConfig = {
8
+ cpus: number;
9
+ memoryMB: number;
10
+ rootfsPath: string;
11
+ sessionDir: string;
12
+ networkAllowlist: NetworkRule[];
13
+ vsockPort: number;
14
+ };
15
+ export type VmInfo = {
16
+ id: string;
17
+ status: "RUNNING" | "STOPPED" | "ERROR";
18
+ pid?: number;
19
+ vsockCid?: number;
20
+ };
21
+ export type ExecResult = {
22
+ stdout: string;
23
+ stderr: string;
24
+ exitCode: number;
25
+ };
26
+ export type VmProvider = {
27
+ readonly name: string;
28
+ isAvailable(): Promise<boolean>;
29
+ start(id: string, config: VmConfig): Promise<VmInfo>;
30
+ exec(id: string, cmd: string, args?: string[]): Promise<ExecResult>;
31
+ /** Spawn a long-lived process with piped stdout/stderr (for agent execution via SSH) */
32
+ spawnStreaming(id: string, cmd: string, args: string[], cwd: string, options?: {
33
+ reverseTunnels?: Array<{
34
+ remotePort: number;
35
+ localPort: number;
36
+ }>;
37
+ }): {
38
+ pid: number;
39
+ process: ChildProcess;
40
+ };
41
+ stop(id: string): Promise<void>;
42
+ destroy(id: string): Promise<void>;
43
+ getInfo(id: string): Promise<VmInfo | null>;
44
+ /** Get the vsock socket path (Unix domain socket on macOS, TCP host:port on Linux/Windows) */
45
+ getVsockPath(id: string): string;
46
+ };
47
+ export type SessionConfig = {
48
+ repoUrl?: string;
49
+ branch?: string;
50
+ workflowBaseBranch?: string;
51
+ taskContext?: Record<string, unknown>;
52
+ workflowContext?: {
53
+ workflowId: string;
54
+ stepId: string;
55
+ };
56
+ };
57
+ export type SessionFiles = {
58
+ contextJson: string;
59
+ mcpConfigJson: string;
60
+ skillFiles: Array<{
61
+ relativePath: string;
62
+ content: string;
63
+ }>;
64
+ };
65
+ export type SessionInfo = {
66
+ id: string;
67
+ taskId: string;
68
+ vmInfo: VmInfo | null;
69
+ sessionDir: string;
70
+ config: SessionConfig;
71
+ status: "CREATED" | "BOOTING" | "RUNNING" | "STOPPED" | "DESTROYED";
72
+ };
73
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,81 @@
1
+ /**
2
+ * VmHelperClient — parent-side IPC client for the VM helper process.
3
+ *
4
+ * Provides the same API as SessionManager but delegates all native addon
5
+ * operations to a child process (node-vm) that has Apple Virtualization
6
+ * entitlements. Communication uses Node.js built-in IPC with structured
7
+ * clone for efficient Buffer transfer.
8
+ */
9
+ import { EventEmitter } from "node:events";
10
+ import { PassThrough } from "node:stream";
11
+ import type { AgentProcess } from "../orchestrator/connectors/connector.js";
12
+ import type { SessionState } from "./vm-ipc-protocol.js";
13
+ /**
14
+ * Parent-side process proxy that receives streaming data via IPC events.
15
+ * Implements AgentProcess so the orchestrator can use it identically.
16
+ */
17
+ export declare class RemoteVmProcessProxy extends EventEmitter implements AgentProcess {
18
+ private readonly taskId;
19
+ private readonly client;
20
+ readonly stdout: PassThrough;
21
+ readonly stderr: PassThrough;
22
+ pid: number;
23
+ exitCode: number | null;
24
+ killed: boolean;
25
+ constructor(taskId: string, client: VmHelperClient);
26
+ setPid(pid: number): void;
27
+ pushStdout(data: Buffer): void;
28
+ pushStderr(data: Buffer): void;
29
+ emitExit(code: number): void;
30
+ kill(signal?: string): boolean;
31
+ }
32
+ export declare class VmHelperClient {
33
+ private child;
34
+ private pending;
35
+ private proxies;
36
+ private exitListeners;
37
+ private _vmRunning;
38
+ /**
39
+ * Spawn the helper child process.
40
+ * Must be called before any other method.
41
+ */
42
+ spawn(): Promise<void>;
43
+ ensureVmRunning(): Promise<void>;
44
+ createSession(taskId: string): Promise<string>;
45
+ cloneRepo(sessionId: string, repoUrl: string | undefined, branch?: string, baseBranch?: string): Promise<void>;
46
+ /**
47
+ * Write session files to the VirtioFS-shared host directory.
48
+ * Stays in parent process — no IPC needed (pure filesystem I/O).
49
+ */
50
+ prepareWorkspace(sessionId: string, files: {
51
+ contextJson: string;
52
+ mcpConfigJson: string;
53
+ skillFiles: Array<{
54
+ relativePath: string;
55
+ content: string;
56
+ }>;
57
+ }): void;
58
+ spawnAgent(taskId: string, cmd: string, args: string[], env?: Record<string, string>): Promise<RemoteVmProcessProxy>;
59
+ killAgent(taskIdOrSessionId: string, signal?: string): Promise<void>;
60
+ destroySession(taskId: string): Promise<void>;
61
+ exec(cmd: string, args: string[], cwd?: string): Promise<{
62
+ stdout: string;
63
+ stderr: string;
64
+ exitCode: number;
65
+ }>;
66
+ shutdown(): Promise<void>;
67
+ /** Send a request on a specific child reference (used during shutdown) */
68
+ private requestOnChild;
69
+ isVmRunning(): boolean;
70
+ getAllSessions(): SessionState[];
71
+ getSessionByTaskId(_taskId: string): SessionState | undefined;
72
+ onSessionExit(sessionId: string, callback: (code: number) => void): void;
73
+ private request;
74
+ private handleMessage;
75
+ private handleResponse;
76
+ private handleEvent;
77
+ private handleChildDeath;
78
+ }
79
+ export declare function initVmHelper(): VmHelperClient;
80
+ export declare function getVmHelper(): VmHelperClient;
81
+ //# sourceMappingURL=vm-helper-client.d.ts.map
@@ -0,0 +1,335 @@
1
+ /**
2
+ * VmHelperClient — parent-side IPC client for the VM helper process.
3
+ *
4
+ * Provides the same API as SessionManager but delegates all native addon
5
+ * operations to a child process (node-vm) that has Apple Virtualization
6
+ * entitlements. Communication uses Node.js built-in IPC with structured
7
+ * clone for efficient Buffer transfer.
8
+ */
9
+ import { fork } from "node:child_process";
10
+ import { randomUUID } from "node:crypto";
11
+ import { EventEmitter } from "node:events";
12
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
13
+ import { homedir } from "node:os";
14
+ import { join, resolve } from "node:path";
15
+ import { PassThrough } from "node:stream";
16
+ import { vmLog } from "./log.js";
17
+ // ---------------------------------------------------------------------------
18
+ // Paths
19
+ // ---------------------------------------------------------------------------
20
+ const SESSIONS_DIR = join(homedir(), ".forgeai", "vm", "sessions");
21
+ // ---------------------------------------------------------------------------
22
+ // RemoteVmProcessProxy
23
+ // ---------------------------------------------------------------------------
24
+ /**
25
+ * Parent-side process proxy that receives streaming data via IPC events.
26
+ * Implements AgentProcess so the orchestrator can use it identically.
27
+ */
28
+ export class RemoteVmProcessProxy extends EventEmitter {
29
+ taskId;
30
+ client;
31
+ stdout;
32
+ stderr;
33
+ pid = -1;
34
+ exitCode = null;
35
+ killed = false;
36
+ constructor(taskId, client) {
37
+ super();
38
+ this.taskId = taskId;
39
+ this.client = client;
40
+ this.stdout = new PassThrough();
41
+ this.stderr = new PassThrough();
42
+ }
43
+ setPid(pid) {
44
+ this.pid = pid;
45
+ }
46
+ pushStdout(data) {
47
+ this.stdout.push(data);
48
+ }
49
+ pushStderr(data) {
50
+ this.stderr.push(data);
51
+ }
52
+ emitExit(code) {
53
+ this.exitCode = code;
54
+ this.stdout.end();
55
+ this.stderr.end();
56
+ this.emit("exit", code, null);
57
+ this.emit("close", code, null);
58
+ }
59
+ kill(signal) {
60
+ if (this.exitCode !== null || this.killed)
61
+ return false;
62
+ this.killed = true;
63
+ this.client.killAgent(this.taskId, signal ?? "SIGTERM").catch(() => { });
64
+ return true;
65
+ }
66
+ }
67
+ // ---------------------------------------------------------------------------
68
+ // VmHelperClient
69
+ // ---------------------------------------------------------------------------
70
+ export class VmHelperClient {
71
+ child = null;
72
+ pending = new Map();
73
+ proxies = new Map();
74
+ exitListeners = new Map();
75
+ _vmRunning = false;
76
+ /**
77
+ * Spawn the helper child process.
78
+ * Must be called before any other method.
79
+ */
80
+ async spawn() {
81
+ const cliRoot = join(import.meta.dirname, "..", "..");
82
+ const nodeVmPath = join(cliRoot, "native", "node-vm");
83
+ const helperScript = join(import.meta.dirname, "vm-helper.js");
84
+ if (!existsSync(nodeVmPath)) {
85
+ throw new Error(`node-vm not found at ${nodeVmPath}. Build it with:\n` +
86
+ " cd packages/cli/native && cp $(which node) node-vm\n" +
87
+ ' codesign --entitlements entitlements-vm-only.plist --force -s "Developer ID Application: ..." node-vm');
88
+ }
89
+ this.child = fork(helperScript, [], {
90
+ execPath: nodeVmPath,
91
+ serialization: "advanced",
92
+ stdio: ["pipe", "pipe", "pipe", "ipc"],
93
+ env: process.env,
94
+ });
95
+ // Forward child stdout/stderr to logs
96
+ this.child.stdout?.on("data", (chunk) => {
97
+ const text = chunk.toString("utf-8").trimEnd();
98
+ if (text)
99
+ vmLog.info("vm-helper:stdout", text);
100
+ });
101
+ this.child.stderr?.on("data", (chunk) => {
102
+ const text = chunk.toString("utf-8").trimEnd();
103
+ if (text)
104
+ vmLog.info("vm-helper:stderr", text);
105
+ });
106
+ // Handle IPC messages from child
107
+ this.child.on("message", (msg) => {
108
+ this.handleMessage(msg);
109
+ });
110
+ // Handle child crash
111
+ this.child.on("exit", (code, signal) => {
112
+ vmLog.error("vm-helper-client", `Helper process exited (code=${code}, signal=${signal})`);
113
+ this.handleChildDeath();
114
+ });
115
+ vmLog.info("vm-helper-client", "Helper process spawned");
116
+ }
117
+ // -------------------------------------------------------------------------
118
+ // SessionManager-compatible API
119
+ // -------------------------------------------------------------------------
120
+ async ensureVmRunning() {
121
+ await this.request({ method: "ensureVmRunning" }, 120_000);
122
+ this._vmRunning = true;
123
+ }
124
+ async createSession(taskId) {
125
+ return (await this.request({ method: "createSession", taskId }, 30_000));
126
+ }
127
+ async cloneRepo(sessionId, repoUrl, branch, baseBranch) {
128
+ await this.request({ method: "cloneRepo", sessionId, repoUrl, branch, baseBranch }, 600_000);
129
+ }
130
+ /**
131
+ * Write session files to the VirtioFS-shared host directory.
132
+ * Stays in parent process — no IPC needed (pure filesystem I/O).
133
+ */
134
+ prepareWorkspace(sessionId, files) {
135
+ const sessionDir = join(SESSIONS_DIR, sessionId);
136
+ mkdirSync(sessionDir, { recursive: true, mode: 0o700 });
137
+ writeFileSync(join(sessionDir, "context.json"), files.contextJson, "utf-8");
138
+ writeFileSync(join(sessionDir, "mcp_config.json"), files.mcpConfigJson, "utf-8");
139
+ for (const file of files.skillFiles) {
140
+ const filePath = resolve(sessionDir, "skills", file.relativePath);
141
+ if (!filePath.startsWith(resolve(sessionDir))) {
142
+ throw new Error(`Path traversal detected: ${file.relativePath}`);
143
+ }
144
+ mkdirSync(join(filePath, ".."), { recursive: true });
145
+ writeFileSync(filePath, file.content, "utf-8");
146
+ }
147
+ }
148
+ async spawnAgent(taskId, cmd, args, env = {}) {
149
+ const proxy = new RemoteVmProcessProxy(taskId, this);
150
+ this.proxies.set(taskId, proxy);
151
+ const result = (await this.request({ method: "spawnAgent", taskId, cmd, args, env }, 30_000));
152
+ proxy.setPid(result.pid);
153
+ return proxy;
154
+ }
155
+ async killAgent(taskIdOrSessionId, signal = "SIGTERM") {
156
+ await this.request({ method: "killAgent", taskIdOrSessionId, signal }, 10_000);
157
+ }
158
+ async destroySession(taskId) {
159
+ await this.request({ method: "destroySession", taskId }, 30_000);
160
+ this.proxies.delete(taskId);
161
+ }
162
+ async exec(cmd, args, cwd) {
163
+ return (await this.request({ method: "exec", cmd, args, cwd }, 600_000));
164
+ }
165
+ async shutdown() {
166
+ if (!this.child)
167
+ return;
168
+ const child = this.child;
169
+ this.child = null;
170
+ this._vmRunning = false;
171
+ try {
172
+ if (child.connected) {
173
+ await this.requestOnChild(child, { method: "shutdown" }, 10_000);
174
+ }
175
+ }
176
+ catch {
177
+ /* child may already be gone */
178
+ }
179
+ try {
180
+ child.kill("SIGTERM");
181
+ }
182
+ catch {
183
+ /* already dead */
184
+ }
185
+ }
186
+ /** Send a request on a specific child reference (used during shutdown) */
187
+ requestOnChild(child, params, timeoutMs) {
188
+ if (!child.connected)
189
+ return Promise.resolve(null);
190
+ const reqId = randomUUID();
191
+ const msg = { type: "req", reqId, ...params };
192
+ return new Promise((resolve, reject) => {
193
+ const timer = setTimeout(() => {
194
+ this.pending.delete(reqId);
195
+ resolve(null); // Don't reject on shutdown timeout
196
+ }, timeoutMs);
197
+ this.pending.set(reqId, { resolve, reject, timer });
198
+ try {
199
+ child.send(msg);
200
+ }
201
+ catch {
202
+ clearTimeout(timer);
203
+ this.pending.delete(reqId);
204
+ resolve(null);
205
+ }
206
+ });
207
+ }
208
+ isVmRunning() {
209
+ return this._vmRunning && this.child !== null;
210
+ }
211
+ getAllSessions() {
212
+ // Synchronous — returns empty if not available (best effort)
213
+ // For accurate data, use the async version
214
+ return [];
215
+ }
216
+ getSessionByTaskId(_taskId) {
217
+ return undefined;
218
+ }
219
+ onSessionExit(sessionId, callback) {
220
+ this.exitListeners.set(sessionId, callback);
221
+ }
222
+ // -------------------------------------------------------------------------
223
+ // IPC internals
224
+ // -------------------------------------------------------------------------
225
+ request(params, timeoutMs) {
226
+ if (!this.child?.connected) {
227
+ return Promise.reject(new Error("VM helper process not connected"));
228
+ }
229
+ const reqId = randomUUID();
230
+ const msg = { type: "req", reqId, ...params };
231
+ return new Promise((resolve, reject) => {
232
+ const timer = setTimeout(() => {
233
+ this.pending.delete(reqId);
234
+ reject(new Error(`IPC timeout (${timeoutMs}ms): ${params.method}`));
235
+ }, timeoutMs);
236
+ this.pending.set(reqId, { resolve, reject, timer });
237
+ this.child.send(msg);
238
+ });
239
+ }
240
+ handleMessage(msg) {
241
+ switch (msg.type) {
242
+ case "res":
243
+ this.handleResponse(msg);
244
+ break;
245
+ case "stream":
246
+ case "exit":
247
+ case "log":
248
+ this.handleEvent(msg);
249
+ break;
250
+ }
251
+ }
252
+ handleResponse(res) {
253
+ const pending = this.pending.get(res.reqId);
254
+ if (!pending)
255
+ return;
256
+ this.pending.delete(res.reqId);
257
+ clearTimeout(pending.timer);
258
+ if (res.ok) {
259
+ pending.resolve(res.value);
260
+ }
261
+ else {
262
+ pending.reject(new Error(res.error));
263
+ }
264
+ }
265
+ handleEvent(event) {
266
+ switch (event.type) {
267
+ case "stream": {
268
+ const proxy = this.proxies.get(event.taskId);
269
+ if (!proxy)
270
+ break;
271
+ if (event.channel === "stdout") {
272
+ proxy.pushStdout(Buffer.from(event.data));
273
+ }
274
+ else {
275
+ proxy.pushStderr(Buffer.from(event.data));
276
+ }
277
+ break;
278
+ }
279
+ case "exit": {
280
+ const proxy = this.proxies.get(event.taskId);
281
+ if (proxy) {
282
+ proxy.emitExit(event.code);
283
+ this.proxies.delete(event.taskId);
284
+ }
285
+ // Also fire exit listener (used by onSessionExit registrations)
286
+ const listener = this.exitListeners.get(event.taskId);
287
+ if (listener) {
288
+ this.exitListeners.delete(event.taskId);
289
+ listener(event.code);
290
+ }
291
+ break;
292
+ }
293
+ case "log": {
294
+ const logFn = event.level === "error"
295
+ ? vmLog.error
296
+ : event.level === "warn"
297
+ ? vmLog.warn
298
+ : vmLog.info;
299
+ logFn.call(vmLog, event.prefix, event.message);
300
+ break;
301
+ }
302
+ }
303
+ }
304
+ handleChildDeath() {
305
+ // Reject all pending requests
306
+ for (const [reqId, pending] of this.pending) {
307
+ clearTimeout(pending.timer);
308
+ pending.reject(new Error("VM helper process died"));
309
+ this.pending.delete(reqId);
310
+ }
311
+ // Emit exit on all active proxies
312
+ for (const [taskId, proxy] of this.proxies) {
313
+ proxy.emitExit(-1);
314
+ this.proxies.delete(taskId);
315
+ }
316
+ this.child = null;
317
+ this._vmRunning = false;
318
+ }
319
+ }
320
+ // ---------------------------------------------------------------------------
321
+ // Singleton
322
+ // ---------------------------------------------------------------------------
323
+ let helperInstance = null;
324
+ export function initVmHelper() {
325
+ if (helperInstance)
326
+ return helperInstance;
327
+ helperInstance = new VmHelperClient();
328
+ return helperInstance;
329
+ }
330
+ export function getVmHelper() {
331
+ if (!helperInstance)
332
+ throw new Error("VmHelperClient not initialized");
333
+ return helperInstance;
334
+ }
335
+ //# sourceMappingURL=vm-helper-client.js.map
@@ -0,0 +1,11 @@
1
+ /**
2
+ * VM Helper Process — child process entry point.
3
+ *
4
+ * Runs under node-vm (signed with com.apple.security.virtualization entitlement).
5
+ * Loads the native addon, creates a real SessionManager, and proxies commands
6
+ * from the parent CLI process over Node.js IPC.
7
+ *
8
+ * Usage: node-vm dist/vm/vm-helper.js
9
+ */
10
+ export {};
11
+ //# sourceMappingURL=vm-helper.d.ts.map