@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,140 @@
1
+ import { EventEmitter } from "node:events";
2
+ import { createConnection } from "node:net";
3
+ import chalk from "chalk";
4
+ // Vsock channels
5
+ export const CHANNEL_CONTROL = 0;
6
+ export const CHANNEL_STDOUT = 1;
7
+ export const CHANNEL_STDERR = 2;
8
+ export const CHANNEL_MCP = 3;
9
+ const HEADER_SIZE = 5; // 1 byte channel + 4 bytes length
10
+ const MAX_FRAME_SIZE = 16 * 1024 * 1024; // 16MB
11
+ export class VsockBridge extends EventEmitter {
12
+ socketPath;
13
+ socket = null;
14
+ buffer = Buffer.alloc(0);
15
+ connected = false;
16
+ /**
17
+ * @param socketPath Unix domain socket path (vfkit exposes vsock as UDS on macOS)
18
+ */
19
+ constructor(socketPath) {
20
+ super();
21
+ this.socketPath = socketPath;
22
+ }
23
+ async connect() {
24
+ return new Promise((resolve, reject) => {
25
+ // vfkit exposes vsock as a Unix domain socket on macOS.
26
+ // QEMU/WSL2 may use TCP — the socketPath can be a path or host:port.
27
+ const socket = createConnection({ path: this.socketPath });
28
+ socket.on("connect", () => {
29
+ this.socket = socket;
30
+ this.connected = true;
31
+ this.setupDataHandler();
32
+ console.log(chalk.green(`[vsock] Connected via ${this.socketPath}`));
33
+ resolve();
34
+ });
35
+ socket.on("error", (err) => {
36
+ if (!this.connected) {
37
+ reject(new Error(`vsock connect failed: ${err.message}`));
38
+ }
39
+ else {
40
+ this.emit("error", err);
41
+ }
42
+ });
43
+ socket.on("close", () => {
44
+ this.connected = false;
45
+ this.emit("close");
46
+ });
47
+ });
48
+ }
49
+ setupDataHandler() {
50
+ if (!this.socket)
51
+ return;
52
+ this.socket.on("data", (data) => {
53
+ this.buffer = Buffer.concat([this.buffer, data]);
54
+ this.processBuffer();
55
+ });
56
+ }
57
+ processBuffer() {
58
+ while (this.buffer.length >= HEADER_SIZE) {
59
+ const channel = this.buffer[0];
60
+ const length = this.buffer.readUInt32BE(1);
61
+ if (length > MAX_FRAME_SIZE) {
62
+ console.warn(chalk.red(`[vsock] Frame too large (${length} bytes), disconnecting`));
63
+ this.disconnect();
64
+ return;
65
+ }
66
+ if (this.buffer.length < HEADER_SIZE + length) {
67
+ break; // Wait for more data
68
+ }
69
+ const payload = this.buffer.subarray(HEADER_SIZE, HEADER_SIZE + length);
70
+ this.buffer = this.buffer.subarray(HEADER_SIZE + length);
71
+ this.handleFrame(channel, payload);
72
+ }
73
+ }
74
+ handleFrame(channel, payload) {
75
+ switch (channel) {
76
+ case CHANNEL_CONTROL: {
77
+ try {
78
+ const msg = JSON.parse(payload.toString());
79
+ // Validate control message shape
80
+ if (typeof msg.type !== "string") {
81
+ console.warn(chalk.yellow("[vsock] Invalid control message: missing type"));
82
+ break;
83
+ }
84
+ if (msg.type === "pid" &&
85
+ (typeof msg.pid !== "number" || msg.pid <= 0)) {
86
+ console.warn(chalk.yellow("[vsock] Invalid PID in control message"));
87
+ break;
88
+ }
89
+ if (msg.type === "exit" && typeof msg.code !== "number") {
90
+ console.warn(chalk.yellow("[vsock] Invalid exit code in control message"));
91
+ break;
92
+ }
93
+ this.emit("control", msg);
94
+ }
95
+ catch {
96
+ console.warn(chalk.yellow("[vsock] Invalid control message JSON"));
97
+ }
98
+ break;
99
+ }
100
+ case CHANNEL_STDOUT:
101
+ this.emit("stdout", payload);
102
+ break;
103
+ case CHANNEL_STDERR:
104
+ this.emit("stderr", payload);
105
+ break;
106
+ case CHANNEL_MCP:
107
+ this.emit("mcp", payload);
108
+ break;
109
+ default:
110
+ console.warn(chalk.yellow(`[vsock] Unknown channel: ${channel}`));
111
+ }
112
+ }
113
+ send(channel, data) {
114
+ if (!this.socket || !this.connected) {
115
+ throw new Error("vsock not connected");
116
+ }
117
+ const payload = typeof data === "string" ? Buffer.from(data) : data;
118
+ const header = Buffer.alloc(HEADER_SIZE);
119
+ header[0] = channel;
120
+ header.writeUInt32BE(payload.length, 1);
121
+ this.socket.write(Buffer.concat([header, payload]));
122
+ }
123
+ sendControl(msg) {
124
+ this.send(CHANNEL_CONTROL, JSON.stringify(msg));
125
+ }
126
+ sendMcp(data) {
127
+ this.send(CHANNEL_MCP, data);
128
+ }
129
+ disconnect() {
130
+ if (this.socket) {
131
+ this.socket.destroy();
132
+ this.socket = null;
133
+ this.connected = false;
134
+ }
135
+ }
136
+ isConnected() {
137
+ return this.connected;
138
+ }
139
+ }
140
+ //# sourceMappingURL=vsock-bridge.js.map
@@ -0,0 +1,98 @@
1
+ /**
2
+ * vsock protocol constants and message types shared between host and VM.
3
+ * The host TypeScript layer (SessionManager) sends HostToVmMessage frames
4
+ * over the control channel; vminitd inside the VM responds with VmToHostMessage frames.
5
+ *
6
+ * Wire format: [4 bytes length BE][JSON payload]
7
+ */
8
+ /** vsock port assignment for the control channel (host <-> vminitd) */
9
+ export declare const VSOCK_CONTROL_PORT = 1024;
10
+ /**
11
+ * Calculate session-specific vsock ports for stdout, stderr, and MCP streams.
12
+ * Each session gets 3 contiguous ports starting at 1025.
13
+ */
14
+ export declare function sessionPorts(sessionIndex: number): {
15
+ stdout: number;
16
+ stderr: number;
17
+ mcp: number;
18
+ };
19
+ /**
20
+ * Base fields shared by all host-to-VM messages.
21
+ * `authToken` is required for control channel authentication (Fix #2).
22
+ * vminitd generates a random token at boot and validates it on every message.
23
+ */
24
+ type HostToVmBase = {
25
+ authToken: string;
26
+ };
27
+ export type SpawnMessage = HostToVmBase & {
28
+ type: "spawn";
29
+ sessionId: string;
30
+ sessionIndex: number;
31
+ cmd: string;
32
+ args: string[];
33
+ env: Record<string, string>;
34
+ cwd: string;
35
+ sandboxConfig: {
36
+ uid: number;
37
+ gid: number;
38
+ workspacePath: string;
39
+ };
40
+ };
41
+ export type ExecMessage = HostToVmBase & {
42
+ type: "exec";
43
+ requestId: string;
44
+ cmd: string;
45
+ args: string[];
46
+ cwd?: string;
47
+ env?: Record<string, string>;
48
+ };
49
+ export type KillMessage = HostToVmBase & {
50
+ type: "kill";
51
+ sessionId: string;
52
+ signal: string;
53
+ };
54
+ export type ShutdownMessage = HostToVmBase & {
55
+ type: "shutdown";
56
+ };
57
+ export type HostToVmMessage = SpawnMessage | ExecMessage | KillMessage | ShutdownMessage;
58
+ export type ReadyMessage = {
59
+ type: "ready";
60
+ };
61
+ export type PidMessage = {
62
+ type: "pid";
63
+ sessionId: string;
64
+ pid: number;
65
+ };
66
+ export type ExitMessage = {
67
+ type: "exit";
68
+ sessionId: string;
69
+ code: number;
70
+ };
71
+ export type ExecResultMessage = {
72
+ type: "exec-result";
73
+ requestId: string;
74
+ stdout: string;
75
+ stderr: string;
76
+ exitCode: number;
77
+ };
78
+ export type ErrorMessage = {
79
+ type: "error";
80
+ sessionId: string;
81
+ message: string;
82
+ };
83
+ export type VmToHostMessage = ReadyMessage | PidMessage | ExitMessage | ExecResultMessage | ErrorMessage;
84
+ /**
85
+ * Encode a message for vsock transmission.
86
+ * Format: [4 bytes length BE][JSON payload as UTF-8]
87
+ */
88
+ export declare function encodeMessage(msg: HostToVmMessage | VmToHostMessage): Buffer;
89
+ /**
90
+ * Frame decoder that accumulates incoming chunks and yields complete messages.
91
+ * Handles partial reads — safe when chunks split in the middle of a frame.
92
+ */
93
+ export declare class FrameDecoder {
94
+ private buffer;
95
+ push(chunk: Buffer): VmToHostMessage[];
96
+ }
97
+ export {};
98
+ //# sourceMappingURL=vsock-protocol.d.ts.map
@@ -0,0 +1,57 @@
1
+ /**
2
+ * vsock protocol constants and message types shared between host and VM.
3
+ * The host TypeScript layer (SessionManager) sends HostToVmMessage frames
4
+ * over the control channel; vminitd inside the VM responds with VmToHostMessage frames.
5
+ *
6
+ * Wire format: [4 bytes length BE][JSON payload]
7
+ */
8
+ /** vsock port assignment for the control channel (host <-> vminitd) */
9
+ export const VSOCK_CONTROL_PORT = 1024;
10
+ /**
11
+ * Calculate session-specific vsock ports for stdout, stderr, and MCP streams.
12
+ * Each session gets 3 contiguous ports starting at 1025.
13
+ */
14
+ export function sessionPorts(sessionIndex) {
15
+ const base = 1025 + sessionIndex * 3;
16
+ return {
17
+ stdout: base,
18
+ stderr: base + 1,
19
+ mcp: base + 2,
20
+ };
21
+ }
22
+ // ---------------------------------------------------------------------------
23
+ // Wire encoding / decoding
24
+ // ---------------------------------------------------------------------------
25
+ /**
26
+ * Encode a message for vsock transmission.
27
+ * Format: [4 bytes length BE][JSON payload as UTF-8]
28
+ */
29
+ export function encodeMessage(msg) {
30
+ const json = JSON.stringify(msg);
31
+ const payload = Buffer.from(json, "utf-8");
32
+ const frame = Buffer.allocUnsafe(4 + payload.length);
33
+ frame.writeUInt32BE(payload.length, 0);
34
+ payload.copy(frame, 4);
35
+ return frame;
36
+ }
37
+ /**
38
+ * Frame decoder that accumulates incoming chunks and yields complete messages.
39
+ * Handles partial reads — safe when chunks split in the middle of a frame.
40
+ */
41
+ export class FrameDecoder {
42
+ buffer = Buffer.alloc(0);
43
+ push(chunk) {
44
+ this.buffer = Buffer.concat([this.buffer, chunk]);
45
+ const messages = [];
46
+ while (this.buffer.length >= 4) {
47
+ const len = this.buffer.readUInt32BE(0);
48
+ if (this.buffer.length < 4 + len)
49
+ break;
50
+ const payload = this.buffer.subarray(4, 4 + len);
51
+ this.buffer = this.buffer.subarray(4 + len);
52
+ messages.push(JSON.parse(payload.toString("utf-8")));
53
+ }
54
+ return messages;
55
+ }
56
+ }
57
+ //# sourceMappingURL=vsock-protocol.js.map
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@getforgeai/cli",
3
+ "version": "0.1.0-beta.1",
4
+ "description": "ForgeAI CLI — runs AI coding agents in local Docker containers with your own AI tokens, and connects them to the ForgeAI cockpit.",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "files": [
10
+ "dist",
11
+ "!dist/**/*.map",
12
+ "rootfs/Dockerfile",
13
+ "rootfs/forge-mcp-relay.mjs"
14
+ ],
15
+ "bin": {
16
+ "forge": "./dist/bin/forgeai.js",
17
+ "forgeai": "./dist/bin/forgeai.js"
18
+ },
19
+ "engines": {
20
+ "node": ">=20.11"
21
+ },
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "homepage": "https://forgeai-production-382c.up.railway.app/docs",
26
+ "bugs": {
27
+ "url": "https://forgeai-production-382c.up.railway.app/contact"
28
+ },
29
+ "keywords": [
30
+ "ai",
31
+ "agents",
32
+ "cli",
33
+ "claude-code",
34
+ "docker",
35
+ "mcp",
36
+ "developer-tools",
37
+ "forgeai"
38
+ ],
39
+ "dependencies": {
40
+ "@inquirer/prompts": "^8.2.1",
41
+ "@modelcontextprotocol/sdk": "^1.27.1",
42
+ "async-mutex": "^0.5.0",
43
+ "better-auth": "^1.4.18",
44
+ "chalk": "^5.6.2",
45
+ "commander": "^14.0.2",
46
+ "open": "^10.2.0",
47
+ "ora": "^8.2.0",
48
+ "socket.io-client": "^4.8.3",
49
+ "zod": "^4.3.6",
50
+ "@getforgeai/protocol": "0.1.0-beta.1"
51
+ },
52
+ "devDependencies": {
53
+ "@types/node": "^25.0.2",
54
+ "@vitest/coverage-v8": "^4.0.18",
55
+ "typescript": "^5.9.3",
56
+ "vitest": "^4.0.15"
57
+ },
58
+ "scripts": {
59
+ "build": "tsc",
60
+ "ts": "tsc --noEmit",
61
+ "dev": "tsc --watch",
62
+ "build:native": "cd native && swift build -c release && node-gyp rebuild",
63
+ "test:ci": "vitest run"
64
+ }
65
+ }
@@ -0,0 +1,50 @@
1
+ FROM ubuntu:24.04
2
+
3
+ ENV DEBIAN_FRONTEND=noninteractive
4
+
5
+ # Base packages
6
+ RUN apt-get update -qq && \
7
+ apt-get install -y -qq --no-install-recommends \
8
+ ca-certificates curl git openssh-client build-essential \
9
+ python3 procps less sudo gnupg && \
10
+ rm -rf /var/lib/apt/lists/*
11
+
12
+ # Node.js 20 LTS via NodeSource
13
+ RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | \
14
+ gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \
15
+ echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | \
16
+ tee /etc/apt/sources.list.d/nodesource.list && \
17
+ apt-get update -qq && \
18
+ apt-get install -y -qq nodejs && \
19
+ rm -rf /var/lib/apt/lists/*
20
+
21
+ # Claude Code CLI
22
+ RUN npm install -g @anthropic-ai/claude-code 2>&1 | tail -3
23
+
24
+ # ForgeAI MCP relay (baked-in copy for reference only — at runtime the CLI
25
+ # stages its own copy into /session/forge-mcp-relay.mjs so the relay version
26
+ # always matches the CLI build, including the MCP bridge auth handshake)
27
+ RUN mkdir -p /opt/forgeai
28
+ COPY forge-mcp-relay.mjs /opt/forgeai/forge-mcp-relay.mjs
29
+
30
+ # Create non-root user (Claude Code refuses --dangerously-skip-permissions as root)
31
+ RUN apt-get update -qq && apt-get install -y -qq --no-install-recommends passwd && \
32
+ rm -rf /var/lib/apt/lists/* && \
33
+ useradd -m -s /bin/bash -u 1001 agent
34
+
35
+ # Working directories (owned by agent user)
36
+ RUN mkdir -p /workspace /session /shared && \
37
+ chown -R agent:agent /workspace /session /shared
38
+
39
+ # Set hostname
40
+ RUN echo "forgeai-agent" > /etc/hostname
41
+
42
+ # Clean up
43
+ RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
44
+
45
+ # Run as non-root user
46
+ USER agent
47
+ WORKDIR /session/workspace
48
+
49
+ # Keep container alive for docker exec
50
+ CMD ["sleep", "infinity"]
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * forge-mcp-relay -- MCP JSON-RPC forwarder for ForgeAI agent sandboxes.
5
+ *
6
+ * This runs INSIDE a bubblewrap sandbox, launched by the agent (e.g. Claude
7
+ * Code) as an MCP server.
8
+ *
9
+ * Data flow:
10
+ * Agent (Claude Code) <--stdin/stdout--> forge-mcp-relay <--TCP--> vminitd <--vsock--> host
11
+ *
12
+ * The relay:
13
+ * 1. Reads JSON-RPC messages from stdin (line-delimited, as per MCP spec)
14
+ * 2. Forwards them over a TCP connection to vminitd's per-session MCP port
15
+ * 3. Reads responses from vminitd and writes them to stdout
16
+ *
17
+ * Environment variables:
18
+ * FORGEAI_MCP_PORT -- TCP port on 127.0.0.1 where vminitd's MCP bridge listens
19
+ * FORGEAI_MCP_TOKEN -- Per-session shared secret; sent as the first line
20
+ * (`FORGEAI_AUTH <token>`) to authenticate to the bridge
21
+ * FORGEAI_SESSION_ID -- Session identifier (for logging)
22
+ */
23
+
24
+ import { createConnection } from "node:net";
25
+ import { createInterface } from "node:readline";
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Configuration
29
+ // ---------------------------------------------------------------------------
30
+
31
+ const MCP_PORT = parseInt(process.argv[2] || process.env.FORGEAI_MCP_PORT || "0", 10);
32
+ const MCP_TOKEN = process.env.FORGEAI_MCP_TOKEN || "";
33
+ const SESSION_ID = process.env.FORGEAI_SESSION_ID ?? "unknown";
34
+
35
+ function log(msg) {
36
+ process.stderr.write(`[mcp-relay:${SESSION_ID}] ${msg}\n`);
37
+ }
38
+
39
+ if (!MCP_PORT) {
40
+ log("ERROR: FORGEAI_MCP_PORT not set or invalid");
41
+ process.exit(1);
42
+ }
43
+
44
+ if (!MCP_TOKEN) {
45
+ log("ERROR: FORGEAI_MCP_TOKEN not set -- cannot authenticate to MCP bridge");
46
+ process.exit(1);
47
+ }
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // TCP connection to vminitd MCP bridge
51
+ // ---------------------------------------------------------------------------
52
+
53
+ let socket = null;
54
+ let connected = false;
55
+ let pendingMessages = [];
56
+
57
+ function connectToVminitd() {
58
+ const MCP_HOST = process.env.FORGEAI_MCP_HOST || "127.0.0.1";
59
+ socket = createConnection({ host: MCP_HOST, port: MCP_PORT }, () => {
60
+ connected = true;
61
+ log(`Connected to vminitd MCP bridge on port ${MCP_PORT}`);
62
+
63
+ // Authenticate FIRST: the bridge requires this exact line before it
64
+ // will process any MCP traffic, and destroys the socket otherwise.
65
+ socket.write(`FORGEAI_AUTH ${MCP_TOKEN}\n`);
66
+
67
+ // Flush any messages that arrived before connection was ready
68
+ for (const msg of pendingMessages) {
69
+ socket.write(msg);
70
+ }
71
+ pendingMessages = [];
72
+ });
73
+
74
+ // Data from vminitd -> stdout (back to the agent)
75
+ socket.on("data", (chunk) => {
76
+ try {
77
+ process.stdout.write(chunk);
78
+ } catch {
79
+ /* stdout closed */
80
+ }
81
+ });
82
+
83
+ socket.on("error", (err) => {
84
+ log(`Socket error: ${err.message}`);
85
+ });
86
+
87
+ socket.on("close", () => {
88
+ connected = false;
89
+ log("Connection to vminitd closed");
90
+ // If vminitd closes the MCP channel, we should exit
91
+ process.exit(0);
92
+ });
93
+ }
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // stdin -> TCP (agent requests to host)
97
+ // ---------------------------------------------------------------------------
98
+
99
+ function startStdinRelay() {
100
+ const rl = createInterface({
101
+ input: process.stdin,
102
+ crlfDelay: Infinity,
103
+ });
104
+
105
+ rl.on("line", (line) => {
106
+ // Forward the line as-is plus newline to maintain JSON-RPC line-delimited format
107
+ const data = line + "\n";
108
+
109
+ if (connected && socket) {
110
+ socket.write(data);
111
+ } else {
112
+ // Queue messages until connected
113
+ pendingMessages.push(data);
114
+ }
115
+ });
116
+
117
+ rl.on("close", () => {
118
+ log("stdin closed (agent disconnected)");
119
+ if (socket) {
120
+ socket.end();
121
+ }
122
+ process.exit(0);
123
+ });
124
+ }
125
+
126
+ // ---------------------------------------------------------------------------
127
+ // Signal handling
128
+ // ---------------------------------------------------------------------------
129
+
130
+ process.on("SIGTERM", () => {
131
+ log("Received SIGTERM, shutting down");
132
+ if (socket) {
133
+ socket.end();
134
+ }
135
+ process.exit(0);
136
+ });
137
+
138
+ process.on("SIGINT", () => {
139
+ log("Received SIGINT, shutting down");
140
+ if (socket) {
141
+ socket.end();
142
+ }
143
+ process.exit(0);
144
+ });
145
+
146
+ // Prevent crashes from propagating
147
+ process.on("uncaughtException", (err) => {
148
+ log(`Uncaught exception: ${err.message}`);
149
+ process.exit(1);
150
+ });
151
+
152
+ // ---------------------------------------------------------------------------
153
+ // Main
154
+ // ---------------------------------------------------------------------------
155
+
156
+ log(`Starting MCP relay (port=${MCP_PORT})`);
157
+ connectToVminitd();
158
+ startStdinRelay();