@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,43 @@
1
+ /**
2
+ * Git credential injection for in-container git operations.
3
+ *
4
+ * The developer's git credential (often a GitHub PAT) must NOT:
5
+ * - appear in the host `docker exec` argv (readable via ps / /proc/<pid>/cmdline)
6
+ * - persist in the cloned repo's .git/config (remote.origin.url), where a
7
+ * prompt-injectable agent could `cat` it out of the container.
8
+ *
9
+ * So we clone with a CLEAN url (no userinfo) and supply the credential
10
+ * out-of-band via an inline git credential helper that reads the value from
11
+ * environment variables. Those env vars are passed to the container with
12
+ * docker's name-only `-e KEY` form (see docker-executor `secretEnv`), so the
13
+ * value is sourced from the daemon's environment and never lands in any argv.
14
+ * The helper script itself only references the env var NAMES.
15
+ *
16
+ * The value therefore exists only transiently, in the environment of the
17
+ * specific CLI-driven git process (clone / push), and nowhere durable.
18
+ */
19
+ export type GitCredentials = {
20
+ username: string;
21
+ password: string;
22
+ };
23
+ /** Env var names the inline credential helper reads at run time. */
24
+ export declare const GIT_CRED_USER_ENV = "FORGE_GIT_USER";
25
+ export declare const GIT_CRED_PASS_ENV = "FORGE_GIT_PASS";
26
+ /**
27
+ * `git -c` arguments that install an inline credential helper.
28
+ *
29
+ * The first (empty) helper resets any helper inherited from the container's
30
+ * git config, so only ours is consulted. The second echoes the credential from
31
+ * the FORGE_GIT_* env vars — the values are expanded by git's own `sh -c` at
32
+ * helper run time, so they never appear in this process's argv.
33
+ */
34
+ export declare function gitCredentialConfigArgs(): string[];
35
+ /**
36
+ * The same inline helper, ready to embed inside a `bash -c "..."` string.
37
+ * Single-quoted so neither the host-side string construction nor the
38
+ * container's bash expands the env vars — only git's helper `sh -c` does.
39
+ */
40
+ export declare function gitCredentialConfigShellFragment(): string;
41
+ /** secretEnv map (name-only docker -e) carrying the credential values. */
42
+ export declare function gitCredentialSecretEnv(creds: GitCredentials): Record<string, string>;
43
+ //# sourceMappingURL=git-credentials.d.ts.map
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Git credential injection for in-container git operations.
3
+ *
4
+ * The developer's git credential (often a GitHub PAT) must NOT:
5
+ * - appear in the host `docker exec` argv (readable via ps / /proc/<pid>/cmdline)
6
+ * - persist in the cloned repo's .git/config (remote.origin.url), where a
7
+ * prompt-injectable agent could `cat` it out of the container.
8
+ *
9
+ * So we clone with a CLEAN url (no userinfo) and supply the credential
10
+ * out-of-band via an inline git credential helper that reads the value from
11
+ * environment variables. Those env vars are passed to the container with
12
+ * docker's name-only `-e KEY` form (see docker-executor `secretEnv`), so the
13
+ * value is sourced from the daemon's environment and never lands in any argv.
14
+ * The helper script itself only references the env var NAMES.
15
+ *
16
+ * The value therefore exists only transiently, in the environment of the
17
+ * specific CLI-driven git process (clone / push), and nowhere durable.
18
+ */
19
+ /** Env var names the inline credential helper reads at run time. */
20
+ export const GIT_CRED_USER_ENV = "FORGE_GIT_USER";
21
+ export const GIT_CRED_PASS_ENV = "FORGE_GIT_PASS";
22
+ /**
23
+ * `git -c` arguments that install an inline credential helper.
24
+ *
25
+ * The first (empty) helper resets any helper inherited from the container's
26
+ * git config, so only ours is consulted. The second echoes the credential from
27
+ * the FORGE_GIT_* env vars — the values are expanded by git's own `sh -c` at
28
+ * helper run time, so they never appear in this process's argv.
29
+ */
30
+ export function gitCredentialConfigArgs() {
31
+ return [
32
+ "-c",
33
+ "credential.helper=",
34
+ "-c",
35
+ `credential.helper=!f() { echo username=$${GIT_CRED_USER_ENV}; echo password=$${GIT_CRED_PASS_ENV}; }; f`,
36
+ ];
37
+ }
38
+ /**
39
+ * The same inline helper, ready to embed inside a `bash -c "..."` string.
40
+ * Single-quoted so neither the host-side string construction nor the
41
+ * container's bash expands the env vars — only git's helper `sh -c` does.
42
+ */
43
+ export function gitCredentialConfigShellFragment() {
44
+ return `-c credential.helper= -c 'credential.helper=!f() { echo username=$${GIT_CRED_USER_ENV}; echo password=$${GIT_CRED_PASS_ENV}; }; f'`;
45
+ }
46
+ /** secretEnv map (name-only docker -e) carrying the credential values. */
47
+ export function gitCredentialSecretEnv(creds) {
48
+ return {
49
+ [GIT_CRED_USER_ENV]: creds.username,
50
+ [GIT_CRED_PASS_ENV]: creds.password,
51
+ };
52
+ }
53
+ //# sourceMappingURL=git-credentials.js.map
@@ -0,0 +1,7 @@
1
+ /** Docker image name for ForgeAI agent containers */
2
+ export declare const FORGEAI_IMAGE: string;
3
+ export declare function imageExists(): Promise<boolean>;
4
+ export declare function pullImage(): Promise<void>;
5
+ export declare function buildImage(): Promise<void>;
6
+ export declare function ensureImage(): Promise<string>;
7
+ //# sourceMappingURL=image-manager.d.ts.map
@@ -0,0 +1,61 @@
1
+ import { execFile } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { promisify } from "node:util";
5
+ import chalk from "chalk";
6
+ const execFileAsync = promisify(execFile);
7
+ /** Docker image name for ForgeAI agent containers */
8
+ export const FORGEAI_IMAGE = process.env.FORGEAI_DOCKER_IMAGE ?? "forgeai/agent:latest";
9
+ /** Path to the Dockerfile for building the agent image */
10
+ function getDockerfilePath() {
11
+ // From dist/vm/ or src/vm/, go up to cli root, then rootfs/
12
+ const cliRoot = join(import.meta.dirname, "..", "..");
13
+ return join(cliRoot, "rootfs");
14
+ }
15
+ export async function imageExists() {
16
+ try {
17
+ await execFileAsync("docker", ["image", "inspect", FORGEAI_IMAGE], {
18
+ timeout: 10_000,
19
+ });
20
+ return true;
21
+ }
22
+ catch {
23
+ return false;
24
+ }
25
+ }
26
+ export async function pullImage() {
27
+ console.log(chalk.cyan(`[image-manager] Pulling Docker image ${FORGEAI_IMAGE}...`));
28
+ try {
29
+ await execFileAsync("docker", ["pull", FORGEAI_IMAGE], {
30
+ timeout: 600_000,
31
+ });
32
+ console.log(chalk.green(`[image-manager] Image pulled: ${FORGEAI_IMAGE}`));
33
+ }
34
+ catch {
35
+ console.log(chalk.yellow(`[image-manager] Pull failed, will try local build`));
36
+ throw new Error(`Failed to pull ${FORGEAI_IMAGE}`);
37
+ }
38
+ }
39
+ export async function buildImage() {
40
+ const contextDir = getDockerfilePath();
41
+ if (!existsSync(join(contextDir, "Dockerfile"))) {
42
+ throw new Error(`Dockerfile not found at ${contextDir}/Dockerfile`);
43
+ }
44
+ console.log(chalk.cyan(`[image-manager] Building Docker image ${FORGEAI_IMAGE}...`));
45
+ await execFileAsync("docker", ["build", "--platform", "linux/arm64", "-t", FORGEAI_IMAGE, contextDir], { timeout: 600_000 });
46
+ console.log(chalk.green(`[image-manager] Image built: ${FORGEAI_IMAGE}`));
47
+ }
48
+ export async function ensureImage() {
49
+ if (await imageExists()) {
50
+ return FORGEAI_IMAGE;
51
+ }
52
+ // Try pull first, fall back to local build
53
+ try {
54
+ await pullImage();
55
+ }
56
+ catch {
57
+ await buildImage();
58
+ }
59
+ return FORGEAI_IMAGE;
60
+ }
61
+ //# sourceMappingURL=image-manager.js.map
@@ -0,0 +1,9 @@
1
+ export declare const vmLog: {
2
+ info: (prefix: string, msg: string, sessionId?: string) => void;
3
+ warn: (prefix: string, msg: string, sessionId?: string) => void;
4
+ error: (prefix: string, msg: string, sessionId?: string) => void;
5
+ dim: (prefix: string, msg: string, sessionId?: string) => void;
6
+ cyan: (prefix: string, msg: string, sessionId?: string) => void;
7
+ magenta: (prefix: string, msg: string, sessionId?: string) => void;
8
+ };
9
+ //# sourceMappingURL=log.d.ts.map
package/dist/vm/log.js ADDED
@@ -0,0 +1,18 @@
1
+ import chalk from "chalk";
2
+ function utc() {
3
+ return new Date().toISOString().replace("T", " ").slice(0, 19) + "Z";
4
+ }
5
+ function fmt(prefix, msg, sessionId) {
6
+ const ts = chalk.dim(utc());
7
+ const sid = sessionId ? chalk.dim(`{${sessionId.slice(0, 20)}}`) + " " : "";
8
+ return `${ts} ${sid}[${prefix}] ${msg}`;
9
+ }
10
+ export const vmLog = {
11
+ info: (prefix, msg, sessionId) => console.log(fmt(prefix, chalk.green(msg), sessionId)),
12
+ warn: (prefix, msg, sessionId) => console.log(fmt(prefix, chalk.yellow(msg), sessionId)),
13
+ error: (prefix, msg, sessionId) => console.log(fmt(prefix, chalk.red(msg), sessionId)),
14
+ dim: (prefix, msg, sessionId) => console.log(fmt(prefix, chalk.dim(msg), sessionId)),
15
+ cyan: (prefix, msg, sessionId) => console.log(fmt(prefix, chalk.cyan(msg), sessionId)),
16
+ magenta: (prefix, msg, sessionId) => console.log(fmt(prefix, chalk.magenta(msg), sessionId)),
17
+ };
18
+ //# sourceMappingURL=log.js.map
@@ -0,0 +1,63 @@
1
+ /**
2
+ * TCP MCP Bridge — Host-side bridge for Docker agent containers.
3
+ *
4
+ * Creates a TCP server on a per-session port. When the in-container
5
+ * forge-mcp-relay connects, pipes the TCP socket to an MCP server
6
+ * running in the daemon process.
7
+ *
8
+ * Data flow:
9
+ * Container: Claude Code → forge-mcp-relay → TCP connect to host
10
+ * Host: TCP socket → PassThrough → MCP server (stdio transport) → Cloud API
11
+ *
12
+ * Security:
13
+ * - The listener binds to 127.0.0.1 (macOS/Windows Docker Desktop forwards
14
+ * host.docker.internal to the host loopback) or the Docker bridge gateway
15
+ * on Linux — never 0.0.0.0, so the port is not exposed on the LAN.
16
+ * - Every connection must authenticate with a per-session shared secret
17
+ * before any MCP handling: the first newline-delimited line must be
18
+ * `FORGEAI_AUTH <secret>` (constant-time compare). The secret is passed
19
+ * to the in-container relay via the MCP config env (FORGEAI_MCP_TOKEN),
20
+ * so other containers / processes on the host network cannot invoke
21
+ * ForgeAI tools with the org's credentials.
22
+ */
23
+ import { type Socket } from "node:net";
24
+ import type { McpSessionContext } from "../mcp/types.js";
25
+ export type McpBridgeOptions = {
26
+ /** Override the auth handshake timeout (for tests). */
27
+ authTimeoutMs?: number;
28
+ /** Override the bind host (for tests). */
29
+ bindHost?: string;
30
+ };
31
+ /**
32
+ * Constant-time check that an auth line equals `FORGEAI_AUTH <secret>`.
33
+ * Exported for unit tests.
34
+ */
35
+ export declare function verifyAuthLine(line: string, secret: string): boolean;
36
+ /**
37
+ * Read the first newline-delimited line from a socket and verify it against
38
+ * the per-session secret. Resolves with any bytes received after the auth
39
+ * line (they belong to the MCP stream). Rejects on bad/missing auth, buffer
40
+ * overflow, timeout, or socket close.
41
+ *
42
+ * The socket is paused on success so no data is lost before the caller
43
+ * pipes it into the MCP transport. Exported for unit tests.
44
+ */
45
+ export declare function authenticateSocket(socket: Socket, secret: string, timeoutMs?: number): Promise<Buffer>;
46
+ /**
47
+ * Start a TCP MCP bridge for a session.
48
+ *
49
+ * Creates a TCP server on the given port. When the container's
50
+ * forge-mcp-relay connects and authenticates with the per-session secret,
51
+ * creates an MCP server with all ForgeAI tools and pipes JSON-RPC messages
52
+ * bidirectionally. Unauthenticated connections are destroyed.
53
+ */
54
+ export declare function startMcpBridge(sessionId: string, port: number, context: McpSessionContext, authSecret: string, options?: McpBridgeOptions): Promise<void>;
55
+ /**
56
+ * Stop and clean up an MCP bridge for a session.
57
+ */
58
+ export declare function stopMcpBridge(sessionId: string): Promise<void>;
59
+ /**
60
+ * Stop all active MCP bridges.
61
+ */
62
+ export declare function stopAllMcpBridges(): Promise<void>;
63
+ //# sourceMappingURL=mcp-bridge.d.ts.map
@@ -0,0 +1,243 @@
1
+ /**
2
+ * TCP MCP Bridge — Host-side bridge for Docker agent containers.
3
+ *
4
+ * Creates a TCP server on a per-session port. When the in-container
5
+ * forge-mcp-relay connects, pipes the TCP socket to an MCP server
6
+ * running in the daemon process.
7
+ *
8
+ * Data flow:
9
+ * Container: Claude Code → forge-mcp-relay → TCP connect to host
10
+ * Host: TCP socket → PassThrough → MCP server (stdio transport) → Cloud API
11
+ *
12
+ * Security:
13
+ * - The listener binds to 127.0.0.1 (macOS/Windows Docker Desktop forwards
14
+ * host.docker.internal to the host loopback) or the Docker bridge gateway
15
+ * on Linux — never 0.0.0.0, so the port is not exposed on the LAN.
16
+ * - Every connection must authenticate with a per-session shared secret
17
+ * before any MCP handling: the first newline-delimited line must be
18
+ * `FORGEAI_AUTH <secret>` (constant-time compare). The secret is passed
19
+ * to the in-container relay via the MCP config env (FORGEAI_MCP_TOKEN),
20
+ * so other containers / processes on the host network cannot invoke
21
+ * ForgeAI tools with the org's credentials.
22
+ */
23
+ import { createServer } from "node:net";
24
+ import { timingSafeEqual } from "node:crypto";
25
+ import { PassThrough } from "node:stream";
26
+ import { createMcpServer, createStdioTransport } from "../mcp/mcp-server.js";
27
+ import { getDockerBridgeGatewayIp } from "./docker-executor.js";
28
+ import { vmLog } from "./log.js";
29
+ // Active bridges keyed by sessionId
30
+ const bridges = new Map();
31
+ // ---------------------------------------------------------------------------
32
+ // Auth handshake
33
+ // ---------------------------------------------------------------------------
34
+ const AUTH_PREFIX = "FORGEAI_AUTH ";
35
+ const MAX_AUTH_LINE_BYTES = 1024;
36
+ const DEFAULT_AUTH_TIMEOUT_MS = 10_000;
37
+ /**
38
+ * Constant-time check that an auth line equals `FORGEAI_AUTH <secret>`.
39
+ * Exported for unit tests.
40
+ */
41
+ export function verifyAuthLine(line, secret) {
42
+ const expected = Buffer.from(`${AUTH_PREFIX}${secret}`, "utf8");
43
+ const actual = Buffer.from(line, "utf8");
44
+ if (actual.length !== expected.length)
45
+ return false;
46
+ return timingSafeEqual(actual, expected);
47
+ }
48
+ /**
49
+ * Read the first newline-delimited line from a socket and verify it against
50
+ * the per-session secret. Resolves with any bytes received after the auth
51
+ * line (they belong to the MCP stream). Rejects on bad/missing auth, buffer
52
+ * overflow, timeout, or socket close.
53
+ *
54
+ * The socket is paused on success so no data is lost before the caller
55
+ * pipes it into the MCP transport. Exported for unit tests.
56
+ */
57
+ export function authenticateSocket(socket, secret, timeoutMs = DEFAULT_AUTH_TIMEOUT_MS) {
58
+ return new Promise((resolve, reject) => {
59
+ let buffered = Buffer.alloc(0);
60
+ const cleanup = () => {
61
+ clearTimeout(timer);
62
+ socket.off("data", onData);
63
+ socket.off("close", onClose);
64
+ socket.off("error", onError);
65
+ socket.pause();
66
+ };
67
+ const timer = setTimeout(() => {
68
+ cleanup();
69
+ reject(new Error("Auth handshake timed out"));
70
+ }, timeoutMs);
71
+ const onData = (chunk) => {
72
+ buffered = Buffer.concat([buffered, chunk]);
73
+ const newlineIdx = buffered.indexOf(0x0a);
74
+ if (newlineIdx === -1) {
75
+ if (buffered.length > MAX_AUTH_LINE_BYTES) {
76
+ cleanup();
77
+ reject(new Error("Auth line exceeds maximum length"));
78
+ }
79
+ return;
80
+ }
81
+ if (newlineIdx > MAX_AUTH_LINE_BYTES) {
82
+ cleanup();
83
+ reject(new Error("Auth line exceeds maximum length"));
84
+ return;
85
+ }
86
+ const line = buffered
87
+ .subarray(0, newlineIdx)
88
+ .toString("utf8")
89
+ .replace(/\r$/, "");
90
+ const remainder = Buffer.from(buffered.subarray(newlineIdx + 1));
91
+ cleanup();
92
+ if (verifyAuthLine(line, secret)) {
93
+ resolve(remainder);
94
+ }
95
+ else {
96
+ reject(new Error("Invalid auth line"));
97
+ }
98
+ };
99
+ const onClose = () => {
100
+ cleanup();
101
+ reject(new Error("Socket closed before auth completed"));
102
+ };
103
+ const onError = (err) => {
104
+ cleanup();
105
+ reject(err);
106
+ };
107
+ socket.on("data", onData);
108
+ socket.on("close", onClose);
109
+ socket.on("error", onError);
110
+ });
111
+ }
112
+ // ---------------------------------------------------------------------------
113
+ // Bind host resolution
114
+ // ---------------------------------------------------------------------------
115
+ /**
116
+ * Resolve the host interface the bridge should bind to.
117
+ *
118
+ * - Linux: the Docker bridge gateway (containers reach the host through it
119
+ * via `--add-host host.docker.internal:host-gateway`).
120
+ * - macOS/Windows: Docker Desktop proxies host.docker.internal connections
121
+ * to the host loopback, so 127.0.0.1 is sufficient and keeps the port
122
+ * off the LAN.
123
+ */
124
+ async function resolveBridgeBindHost() {
125
+ if (process.platform === "linux") {
126
+ return getDockerBridgeGatewayIp();
127
+ }
128
+ return "127.0.0.1";
129
+ }
130
+ // ---------------------------------------------------------------------------
131
+ // Public API
132
+ // ---------------------------------------------------------------------------
133
+ /**
134
+ * Start a TCP MCP bridge for a session.
135
+ *
136
+ * Creates a TCP server on the given port. When the container's
137
+ * forge-mcp-relay connects and authenticates with the per-session secret,
138
+ * creates an MCP server with all ForgeAI tools and pipes JSON-RPC messages
139
+ * bidirectionally. Unauthenticated connections are destroyed.
140
+ */
141
+ export async function startMcpBridge(sessionId, port, context, authSecret, options) {
142
+ if (!authSecret) {
143
+ throw new Error("MCP bridge requires a non-empty auth secret");
144
+ }
145
+ // Clean up any existing bridge on this port
146
+ await stopMcpBridge(sessionId);
147
+ const bindHost = options?.bindHost ?? (await resolveBridgeBindHost());
148
+ const authTimeoutMs = options?.authTimeoutMs ?? DEFAULT_AUTH_TIMEOUT_MS;
149
+ return new Promise((resolve, reject) => {
150
+ const tcpServer = createServer((clientSocket) => {
151
+ const remote = `${clientSocket.remoteAddress ?? "?"}:${clientSocket.remotePort ?? "?"}`;
152
+ clientSocket.on("error", (err) => {
153
+ vmLog.warn("mcp-bridge", `Socket error for ${sessionId}: ${err.message}`, sessionId);
154
+ });
155
+ authenticateSocket(clientSocket, authSecret, authTimeoutMs)
156
+ .then((remainder) => {
157
+ vmLog.info("mcp-bridge", `Relay authenticated for session ${sessionId} on port ${port} (${remote})`, sessionId);
158
+ wireMcpServer(sessionId, clientSocket, remainder, context);
159
+ })
160
+ .catch((err) => {
161
+ vmLog.warn("mcp-bridge", `Rejected unauthenticated connection from ${remote} on port ${port}: ${err.message}`, sessionId);
162
+ clientSocket.destroy();
163
+ });
164
+ });
165
+ tcpServer.on("error", (err) => {
166
+ vmLog.error("mcp-bridge", `TCP server error on port ${port}: ${err.message}`, sessionId);
167
+ reject(err);
168
+ });
169
+ tcpServer.listen(port, bindHost, () => {
170
+ vmLog.info("mcp-bridge", `Listening on ${bindHost}:${port} for session ${sessionId}`, sessionId);
171
+ bridges.set(sessionId, {
172
+ port,
173
+ server: tcpServer,
174
+ mcpServer: null,
175
+ socket: null,
176
+ });
177
+ resolve();
178
+ });
179
+ });
180
+ }
181
+ /**
182
+ * Wire an authenticated socket into a fresh MCP server instance.
183
+ */
184
+ function wireMcpServer(sessionId, clientSocket, remainder, context) {
185
+ // Create PassThrough streams to pipe between TCP and MCP server
186
+ const mcpInput = new PassThrough(); // relay → MCP server (requests)
187
+ const mcpOutput = new PassThrough(); // MCP server → relay (responses)
188
+ // Bytes that arrived in the same packet as the auth line belong to the
189
+ // MCP stream — feed them in before piping the live socket.
190
+ if (remainder.length > 0) {
191
+ mcpInput.write(remainder);
192
+ }
193
+ // Pipe TCP socket ↔ PassThrough streams (resumes the paused socket)
194
+ clientSocket.pipe(mcpInput);
195
+ mcpOutput.pipe(clientSocket);
196
+ // Create MCP server with the PassThrough streams as transport
197
+ const mcpServer = createMcpServer(context);
198
+ const transport = createStdioTransport(mcpInput, mcpOutput);
199
+ mcpServer
200
+ .connect(transport)
201
+ .then(() => {
202
+ vmLog.info("mcp-bridge", `MCP server connected for session ${sessionId}`, sessionId);
203
+ })
204
+ .catch((err) => {
205
+ vmLog.error("mcp-bridge", `MCP server connect failed: ${err.message}`, sessionId);
206
+ });
207
+ // Update bridge state
208
+ const bridge = bridges.get(sessionId);
209
+ if (bridge) {
210
+ bridge.mcpServer = mcpServer;
211
+ bridge.socket = clientSocket;
212
+ }
213
+ clientSocket.on("close", () => {
214
+ vmLog.info("mcp-bridge", `Relay disconnected for session ${sessionId}`, sessionId);
215
+ });
216
+ }
217
+ /**
218
+ * Stop and clean up an MCP bridge for a session.
219
+ */
220
+ export async function stopMcpBridge(sessionId) {
221
+ const bridge = bridges.get(sessionId);
222
+ if (!bridge)
223
+ return;
224
+ try {
225
+ if (bridge.socket) {
226
+ bridge.socket.destroy();
227
+ }
228
+ bridge.server.close();
229
+ }
230
+ catch {
231
+ /* best effort */
232
+ }
233
+ bridges.delete(sessionId);
234
+ }
235
+ /**
236
+ * Stop all active MCP bridges.
237
+ */
238
+ export async function stopAllMcpBridges() {
239
+ for (const sessionId of bridges.keys()) {
240
+ await stopMcpBridge(sessionId);
241
+ }
242
+ }
243
+ //# sourceMappingURL=mcp-bridge.js.map
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * forge-mcp-stub — In-VM MCP forwarder for ForgeAI.
4
+ *
5
+ * Launched by Claude Code inside the VM as its MCP server.
6
+ * Reads JSON-RPC from stdin, forwards to host via vsock channel 3,
7
+ * and pipes responses back to stdout.
8
+ *
9
+ * This script contains NO credentials and has NO direct Cloud access.
10
+ * All MCP tool calls are handled by the host-side MCP bridge.
11
+ *
12
+ * Baked into the VM rootfs image at /usr/local/bin/forge-mcp-stub
13
+ */
14
+ export {};
15
+ //# sourceMappingURL=mcp-stub.d.ts.map
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * forge-mcp-stub — In-VM MCP forwarder for ForgeAI.
4
+ *
5
+ * Launched by Claude Code inside the VM as its MCP server.
6
+ * Reads JSON-RPC from stdin, forwards to host via vsock channel 3,
7
+ * and pipes responses back to stdout.
8
+ *
9
+ * This script contains NO credentials and has NO direct Cloud access.
10
+ * All MCP tool calls are handled by the host-side MCP bridge.
11
+ *
12
+ * Baked into the VM rootfs image at /usr/local/bin/forge-mcp-stub
13
+ */
14
+ import { createConnection } from "node:net";
15
+ const CHANNEL_MCP = 3;
16
+ const HEADER_SIZE = 5;
17
+ const VSOCK_PORT = parseInt(process.argv[2] ?? process.env.FORGE_VSOCK_PORT ?? "6789", 10);
18
+ let hostSocket = null;
19
+ let vsockBuffer = Buffer.alloc(0);
20
+ function sendFrame(socket, channel, data) {
21
+ const payload = typeof data === "string" ? Buffer.from(data) : data;
22
+ const header = Buffer.alloc(HEADER_SIZE);
23
+ header[0] = channel;
24
+ header.writeUInt32BE(payload.length, 1);
25
+ socket.write(Buffer.concat([header, payload]));
26
+ }
27
+ function processVsockBuffer() {
28
+ while (vsockBuffer.length >= HEADER_SIZE) {
29
+ const channel = vsockBuffer[0];
30
+ const length = vsockBuffer.readUInt32BE(1);
31
+ if (vsockBuffer.length < HEADER_SIZE + length)
32
+ break;
33
+ const payload = vsockBuffer.subarray(HEADER_SIZE, HEADER_SIZE + length);
34
+ vsockBuffer = vsockBuffer.subarray(HEADER_SIZE + length);
35
+ if (channel === CHANNEL_MCP) {
36
+ // Forward MCP response to Claude Code via stdout
37
+ process.stdout.write(payload);
38
+ }
39
+ }
40
+ }
41
+ // Connect to host via vsock (TCP fallback for development)
42
+ const socket = createConnection({ host: "127.0.0.1", port: VSOCK_PORT }, () => {
43
+ hostSocket = socket;
44
+ // Forward stdin (from Claude Code) to host via vsock MCP channel
45
+ process.stdin.on("data", (data) => {
46
+ if (hostSocket) {
47
+ sendFrame(hostSocket, CHANNEL_MCP, data);
48
+ }
49
+ });
50
+ process.stdin.on("end", () => {
51
+ socket.end();
52
+ process.exit(0);
53
+ });
54
+ });
55
+ // Receive responses from host
56
+ socket.on("data", (data) => {
57
+ vsockBuffer = Buffer.concat([vsockBuffer, data]);
58
+ processVsockBuffer();
59
+ });
60
+ socket.on("error", (err) => {
61
+ process.stderr.write(`[forge-mcp-stub] Connection error: ${err.message}\n`);
62
+ process.exit(1);
63
+ });
64
+ socket.on("close", () => {
65
+ process.exit(0);
66
+ });
67
+ //# sourceMappingURL=mcp-stub.js.map
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Typed wrapper around the native Swift N-API addon (forgevm.node).
3
+ * Loads the prebuilt binary or falls back to a local build directory.
4
+ *
5
+ * The addon exposes low-level VM lifecycle, vsock communication, and VirtioFS
6
+ * mount primitives. This module re-exports them with TypeScript types so the
7
+ * rest of the CLI can use them safely.
8
+ */
9
+ /** Native addon handle type (opaque — only the addon knows the internal shape) */
10
+ export type VmHandle = unknown;
11
+ /** Configuration for VM creation */
12
+ export type NativeVmConfig = {
13
+ cpus: number;
14
+ memoryMB: number;
15
+ diskPath: string;
16
+ efiStorePath: string;
17
+ /** Path to uncompressed ARM64 kernel Image (enables VZLinuxBootLoader) */
18
+ kernelPath?: string;
19
+ /** Path to initrd/initramfs image */
20
+ initrdPath?: string;
21
+ /** Kernel command line (default: "console=hvc0 root=/dev/vda rw quiet") */
22
+ cmdline?: string;
23
+ };
24
+ /** VirtioFS mount configuration */
25
+ export type VirtioFsMount = {
26
+ tag: string;
27
+ hostPath: string;
28
+ readOnly: boolean;
29
+ };
30
+ /** Raw addon interface — matches the N-API exports from the Swift addon */
31
+ type ForgeVmAddon = {
32
+ create(config: NativeVmConfig): VmHandle;
33
+ start(handle: VmHandle): Promise<void>;
34
+ stop(handle: VmHandle): Promise<void>;
35
+ destroy(handle: VmHandle): void;
36
+ /** Enable NAT networking (requires com.apple.vm.networking entitlement + provisioning profile) */
37
+ enableNetwork(handle: VmHandle): void;
38
+ addVirtioFs(handle: VmHandle, tag: string, hostPath: string, readOnly: boolean): void;
39
+ configureVsock(handle: VmHandle): void;
40
+ vsockSend(handle: VmHandle, port: number, data: Buffer): void;
41
+ onVsockData(handle: VmHandle, port: number, callback: (data: Buffer) => void): void;
42
+ /** Register callback for serial console output from the VM */
43
+ onSerialData(handle: VmHandle, callback: (data: Buffer) => void): void;
44
+ /** 0 = created, 1 = booting, 2 = running, 3 = stopping, 4 = stopped, 5 = error */
45
+ getState(handle: VmHandle): number;
46
+ };
47
+ /**
48
+ * Load the native addon synchronously.
49
+ * Searches for the prebuilt binary first, then falls back to a local build.
50
+ *
51
+ * Uses `createRequire` to load `.node` binaries in an ESM context
52
+ * (native addons cannot be loaded with dynamic `import()`).
53
+ */
54
+ export declare function loadNativeAddon(): ForgeVmAddon;
55
+ export declare const VmState: {
56
+ readonly CREATED: 0;
57
+ readonly BOOTING: 1;
58
+ readonly RUNNING: 2;
59
+ readonly STOPPING: 3;
60
+ readonly STOPPED: 4;
61
+ readonly ERROR: 5;
62
+ };
63
+ /**
64
+ * Check whether the native addon binary is available on this machine.
65
+ * Returns false if the addon cannot be loaded (e.g. wrong platform, not built).
66
+ */
67
+ export declare function isAddonAvailable(): boolean;
68
+ export {};
69
+ //# sourceMappingURL=native-addon.d.ts.map