@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,3 @@
1
+ import type { AgentConnector } from "./connector.js";
2
+ export declare const claudeCodeConnector: AgentConnector;
3
+ //# sourceMappingURL=claude-code-connector.d.ts.map
@@ -0,0 +1,60 @@
1
+ import { spawn } from "node:child_process";
2
+ import { stopAgentProcess } from "./connector.js";
3
+ function buildDefaultPrompt(skillName) {
4
+ return `You are a ForgeAI agent. Read and follow the ${skillName} skill in .claude/skills/${skillName}/SKILL.md to implement your assigned task.`;
5
+ }
6
+ function getCleanEnv() {
7
+ const env = { ...process.env };
8
+ // Remove Claude Code session markers so spawned agents don't refuse to start
9
+ // with "cannot be launched inside another Claude Code session"
10
+ delete env.CLAUDECODE;
11
+ delete env.CLAUDE_CODE_ENTRYPOINT;
12
+ return env;
13
+ }
14
+ function spawnClaude(params, prompt) {
15
+ const args = [
16
+ "--mcp-config",
17
+ params.mcpConfigPath,
18
+ "-p",
19
+ prompt,
20
+ "--output-format",
21
+ "stream-json",
22
+ "--verbose",
23
+ "--include-partial-messages",
24
+ ];
25
+ // === DISPATCH DEBUG: log exact spawn args ===
26
+ console.log(`\x1b[35m[claude-spawn-debug] cwd="${params.worktreePath}"\n prompt="${prompt.slice(0, 200)}${prompt.length > 200 ? "..." : ""}"\n args=${JSON.stringify(args.filter((a) => a !== prompt))}\x1b[0m`);
27
+ const child = spawn("claude", args, {
28
+ cwd: params.worktreePath,
29
+ stdio: ["ignore", "pipe", "pipe"],
30
+ detached: false,
31
+ env: getCleanEnv(),
32
+ });
33
+ if (child.pid === undefined) {
34
+ throw new Error("Failed to spawn claude process — no PID assigned");
35
+ }
36
+ return { pid: child.pid, process: child };
37
+ }
38
+ export const claudeCodeConnector = {
39
+ name: "claude-code",
40
+ async start(params) {
41
+ return spawnClaude(params, params.prompt ?? buildDefaultPrompt(params.skillName));
42
+ },
43
+ stop: stopAgentProcess,
44
+ async resume(params, ctx) {
45
+ const basePrompt = params.prompt ?? buildDefaultPrompt(params.skillName);
46
+ const parts = [basePrompt];
47
+ if (ctx?.agentQuestion || ctx?.response) {
48
+ parts.push("\n\n## Human Interaction Resume\n");
49
+ if (ctx.agentQuestion) {
50
+ parts.push(`**Your previous question:**\n${ctx.agentQuestion}\n`);
51
+ }
52
+ if (ctx.response) {
53
+ parts.push(`**Human response:**\n${ctx.response}\n`);
54
+ }
55
+ parts.push("Continue your work using the human's response above. Do not re-ask the same question.");
56
+ }
57
+ return spawnClaude(params, parts.join("\n"));
58
+ },
59
+ };
60
+ //# sourceMappingURL=claude-code-connector.js.map
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Abstract process interface for agent execution.
3
+ * ChildProcess satisfies this interface natively.
4
+ * VmProcessProxy also satisfies it via vsock.
5
+ */
6
+ export type AgentProcess = NodeJS.EventEmitter & {
7
+ readonly stdout: NodeJS.ReadableStream | null;
8
+ readonly stderr: NodeJS.ReadableStream | null;
9
+ readonly pid: number;
10
+ readonly exitCode: number | null;
11
+ readonly killed: boolean;
12
+ /** True when exit was caused by an authentication error */
13
+ readonly isAuthError?: boolean;
14
+ kill(signal?: string): boolean;
15
+ };
16
+ /**
17
+ * Parameters required to spawn an agent.
18
+ */
19
+ export type SpawnParams = {
20
+ taskId: string;
21
+ projectId: string;
22
+ orgId: string;
23
+ workingDir: string;
24
+ mcpConfigPath: string;
25
+ skillName: string;
26
+ /** Custom prompt override — used for workflow step agents */
27
+ prompt?: string;
28
+ };
29
+ /**
30
+ * Extended spawn params for VM connector — includes repo/branch/skill context
31
+ * that the VM connector needs to set up the environment internally.
32
+ */
33
+ export type VmSpawnParams = SpawnParams & {
34
+ repoUrl?: string;
35
+ taskBranch?: string;
36
+ workflowBaseBranch?: string;
37
+ skill?: {
38
+ name: string;
39
+ files: Array<{
40
+ path: string;
41
+ content: string;
42
+ }>;
43
+ };
44
+ taskContext?: Record<string, unknown>;
45
+ workflowContext?: {
46
+ workflowId: string;
47
+ stepId: string;
48
+ };
49
+ /** Base64-encoded session snapshot for --continue resume after timeout */
50
+ sessionSnapshotBase64?: string;
51
+ };
52
+ /**
53
+ * Result of spawning an agent subprocess.
54
+ */
55
+ export type SpawnResult = {
56
+ pid: number;
57
+ process: AgentProcess;
58
+ vmId?: string;
59
+ };
60
+ /**
61
+ * Tracks a running agent subprocess in the registry.
62
+ */
63
+ export type AgentEntry = {
64
+ taskId: string;
65
+ pid: number;
66
+ process: AgentProcess;
67
+ connectorName: string;
68
+ workingDir: string;
69
+ orgId: string;
70
+ startedAt: string;
71
+ status: "RUNNING" | "COMPLETED" | "ERROR" | "IDLE";
72
+ vmId?: string;
73
+ };
74
+ /**
75
+ * Uniform interface for spawning and stopping AI agent subprocesses.
76
+ * Connectors are stateless — they only know how to spawn/stop a specific agent type.
77
+ */
78
+ export type ResumeContext = {
79
+ response?: string;
80
+ agentQuestion?: string;
81
+ };
82
+ export type AgentConnector = {
83
+ readonly name: string;
84
+ start(params: SpawnParams | VmSpawnParams): Promise<SpawnResult>;
85
+ stop(entry: AgentEntry): Promise<void>;
86
+ resume(params: SpawnParams | VmSpawnParams, ctx?: ResumeContext): Promise<SpawnResult>;
87
+ /** Resume a workflow step from a session snapshot (--continue in new container) */
88
+ resumeWithSnapshot?(params: VmSpawnParams): Promise<SpawnResult>;
89
+ };
90
+ /**
91
+ * Shared stop logic: SIGTERM → wait up to 10s → SIGKILL if still alive.
92
+ * Used by all connectors to avoid duplicating the stop sequence.
93
+ */
94
+ export declare function stopAgentProcess(entry: AgentEntry): Promise<void>;
95
+ //# sourceMappingURL=connector.d.ts.map
@@ -0,0 +1,25 @@
1
+ const STOP_TIMEOUT_MS = 10_000;
2
+ /**
3
+ * Shared stop logic: SIGTERM → wait up to 10s → SIGKILL if still alive.
4
+ * Used by all connectors to avoid duplicating the stop sequence.
5
+ */
6
+ export async function stopAgentProcess(entry) {
7
+ const { process: proc } = entry;
8
+ if (proc.exitCode !== null || proc.killed) {
9
+ return;
10
+ }
11
+ proc.kill("SIGTERM");
12
+ await new Promise((resolve) => {
13
+ const timeout = setTimeout(() => {
14
+ if (proc.exitCode === null && !proc.killed) {
15
+ proc.kill("SIGKILL");
16
+ }
17
+ resolve();
18
+ }, STOP_TIMEOUT_MS);
19
+ proc.once("exit", () => {
20
+ clearTimeout(timeout);
21
+ resolve();
22
+ });
23
+ });
24
+ }
25
+ //# sourceMappingURL=connector.js.map
@@ -0,0 +1,14 @@
1
+ /**
2
+ * AgentConnector implementation that runs agents inside Docker containers.
3
+ *
4
+ * Each agent session gets its own container with a full git clone.
5
+ * Communication uses native Docker exec stdio streams.
6
+ * MCP bridge via TCP to host (forge-mcp-relay inside container).
7
+ *
8
+ * Container lifecycle is workflow-aware:
9
+ * - Board tasks: container destroyed immediately on agent exit
10
+ * - Workflow steps: container kept alive during idle window for --continue resume
11
+ */
12
+ import type { AgentConnector } from "./connector.js";
13
+ export declare const dockerConnector: AgentConnector;
14
+ //# sourceMappingURL=docker-connector.d.ts.map
@@ -0,0 +1,337 @@
1
+ /**
2
+ * AgentConnector implementation that runs agents inside Docker containers.
3
+ *
4
+ * Each agent session gets its own container with a full git clone.
5
+ * Communication uses native Docker exec stdio streams.
6
+ * MCP bridge via TCP to host (forge-mcp-relay inside container).
7
+ *
8
+ * Container lifecycle is workflow-aware:
9
+ * - Board tasks: container destroyed immediately on agent exit
10
+ * - Workflow steps: container kept alive during idle window for --continue resume
11
+ */
12
+ import { stopAgentProcess } from "./connector.js";
13
+ import { getVmManager } from "../../vm/vm-manager-facade.js";
14
+ import { startMcpBridge, stopMcpBridge } from "../../vm/mcp-bridge.js";
15
+ import { getDockerBridgeGatewayIp } from "../../vm/docker-executor.js";
16
+ import { activeConnections } from "../../lib/connection-manager.js";
17
+ import { vmLog } from "../../vm/log.js";
18
+ // ---------------------------------------------------------------------------
19
+ // Helpers
20
+ // ---------------------------------------------------------------------------
21
+ /**
22
+ * Resolve the host address the in-container relay connects to.
23
+ * Must match the interface the MCP bridge binds to (see mcp-bridge.ts):
24
+ * the Docker bridge gateway on Linux, host.docker.internal elsewhere
25
+ * (Docker Desktop forwards it to the host loopback).
26
+ */
27
+ async function resolveMcpRelayHost() {
28
+ if (process.platform === "linux") {
29
+ return getDockerBridgeGatewayIp();
30
+ }
31
+ return "host.docker.internal";
32
+ }
33
+ /**
34
+ * Build MCP config JSON for the Docker container.
35
+ * The relay connects to the host MCP server via host.docker.internal.
36
+ * Runs the relay staged in the session dir (always matches this CLI build)
37
+ * and authenticates to the bridge with the per-session secret.
38
+ */
39
+ function buildMcpConfigJson(sessionIndex, mcpSecret, mcpHost) {
40
+ const mcpPort = 20000 + sessionIndex;
41
+ return JSON.stringify({
42
+ mcpServers: {
43
+ forgeai: {
44
+ command: "node",
45
+ args: ["/session/forge-mcp-relay.mjs"],
46
+ env: {
47
+ FORGEAI_MCP_HOST: mcpHost,
48
+ FORGEAI_MCP_PORT: String(mcpPort),
49
+ FORGEAI_MCP_TOKEN: mcpSecret,
50
+ },
51
+ },
52
+ },
53
+ }, null, 2);
54
+ }
55
+ function buildDefaultPrompt(skillName) {
56
+ return `You are a ForgeAI agent. Read and follow the ${skillName} skill in .claude/skills/${skillName}/SKILL.md to implement your assigned task.`;
57
+ }
58
+ function buildResumePrompt(basePrompt, ctx) {
59
+ const parts = [basePrompt];
60
+ if (ctx?.agentQuestion ?? ctx?.response) {
61
+ parts.push("\n\n## Human Interaction Resume\n");
62
+ if (ctx?.agentQuestion) {
63
+ parts.push(`**Your previous question:**\n${ctx.agentQuestion}\n`);
64
+ }
65
+ if (ctx?.response) {
66
+ parts.push(`**Human response:**\n${ctx.response}\n`);
67
+ }
68
+ parts.push("Continue your work using the human's response above. Do not re-ask the same question.");
69
+ }
70
+ return parts.join("\n");
71
+ }
72
+ /** Standard Claude CLI args for agent execution */
73
+ function buildAgentArgs(mcpConfigPath, prompt) {
74
+ return [
75
+ "--dangerously-skip-permissions",
76
+ "--mcp-config",
77
+ mcpConfigPath,
78
+ "-p",
79
+ prompt,
80
+ "--output-format",
81
+ "stream-json",
82
+ "--verbose",
83
+ "--include-partial-messages",
84
+ ];
85
+ }
86
+ /** Claude CLI args for --continue resume (requires -p for print/stream mode) */
87
+ function buildContinueArgs(mcpConfigPath, prompt) {
88
+ return [
89
+ "--continue",
90
+ "--dangerously-skip-permissions",
91
+ "--mcp-config",
92
+ mcpConfigPath,
93
+ "-p",
94
+ prompt,
95
+ "--output-format",
96
+ "stream-json",
97
+ "--verbose",
98
+ "--include-partial-messages",
99
+ ];
100
+ }
101
+ /**
102
+ * Start the MCP bridge for a session.
103
+ * Returns the MCP port used, or null if no active connection available.
104
+ */
105
+ async function startSessionMcpBridge(session, params) {
106
+ const mcpPort = 20000 + session.sessionIndex;
107
+ const orgConn = activeConnections.get(params.orgId);
108
+ if (orgConn) {
109
+ await startMcpBridge(session.id, mcpPort, {
110
+ taskId: params.taskId,
111
+ projectId: params.projectId,
112
+ orgId: params.orgId,
113
+ restClient: orgConn.restClient,
114
+ socketClient: orgConn.socketClient,
115
+ currentTask: null,
116
+ projectRoot: null,
117
+ workflowId: params.workflowContext?.workflowId,
118
+ stepId: params.workflowContext?.stepId,
119
+ }, session.mcpSecret);
120
+ return mcpPort;
121
+ }
122
+ vmLog.warn("docker-connector", `No active connection for org ${params.orgId} — MCP tools unavailable`, session.id);
123
+ return null;
124
+ }
125
+ // ---------------------------------------------------------------------------
126
+ // Connector
127
+ // ---------------------------------------------------------------------------
128
+ export const dockerConnector = {
129
+ name: "docker-claude-code",
130
+ async start(params) {
131
+ const sm = getVmManager();
132
+ // 1. Ensure Docker is ready + image available
133
+ await sm.ensureVmRunning();
134
+ // 2. Create session (container + host dirs)
135
+ const sessionId = await sm.createSession(params.taskId);
136
+ const session = sm.getSessionByTaskId(params.taskId);
137
+ // 3. Clone repo inside the container
138
+ await sm.cloneRepo(sessionId, params.repoUrl, params.taskBranch, params.workflowBaseBranch);
139
+ // 4. Prepare workspace files on the host (volume-mounted at /session)
140
+ const skillFiles = [];
141
+ if (params.skill) {
142
+ for (const file of params.skill.files) {
143
+ skillFiles.push({
144
+ relativePath: `${params.skill.name}/${file.path}`,
145
+ content: file.content,
146
+ });
147
+ }
148
+ }
149
+ sm.prepareWorkspace(sessionId, {
150
+ contextJson: JSON.stringify(params.taskContext ?? {}, null, 2),
151
+ mcpConfigJson: buildMcpConfigJson(session.sessionIndex, session.mcpSecret, await resolveMcpRelayHost()),
152
+ skillFiles,
153
+ });
154
+ // 5. Copy workspace files from volume mount into /workspace inside container
155
+ await sm.exec(params.taskId, "bash", [
156
+ "-c",
157
+ [
158
+ "mkdir -p /session/workspace/.forgeai /session/workspace/.claude",
159
+ "cp /session/context.json /session/workspace/.forgeai/context.json",
160
+ "cp /session/mcp_config.json /session/workspace/.claude/mcp_config.json",
161
+ ].join(" && "),
162
+ ]);
163
+ if (skillFiles.length > 0) {
164
+ await sm.exec(params.taskId, "bash", [
165
+ "-c",
166
+ [
167
+ "mkdir -p /session/workspace/.claude/skills",
168
+ "cp -r /session/skills/. /session/workspace/.claude/skills/",
169
+ ].join(" && "),
170
+ ]);
171
+ }
172
+ // 6. Start MCP bridge (TCP server on host for forge-mcp-relay in container)
173
+ await startSessionMcpBridge(session, params);
174
+ // 7. Spawn agent
175
+ const prompt = params.prompt ?? buildDefaultPrompt(params.skillName);
176
+ const agentArgs = buildAgentArgs("/session/workspace/.claude/mcp_config.json", prompt);
177
+ const env = {};
178
+ vmLog.magenta("docker-connector", `Spawning agent in container ${sessionId}`, sessionId);
179
+ const proxy = await sm.spawnAgent(params.taskId, "claude", agentArgs, env);
180
+ // 8. Container lifecycle on agent exit — depends on workflow context
181
+ if (!params.workflowContext) {
182
+ // Board task: destroy container + MCP bridge immediately on exit
183
+ proxy.once("exit", () => {
184
+ stopMcpBridge(sessionId).catch(() => { });
185
+ sm.destroySession(params.taskId).catch((err) => {
186
+ const message = err instanceof Error ? err.message : String(err);
187
+ vmLog.warn("docker-connector", `Cleanup failed for ${sessionId}: ${message}`, sessionId);
188
+ });
189
+ });
190
+ }
191
+ else {
192
+ // Workflow step: keep container alive for idle window.
193
+ // Only stop MCP bridge — container stays for --continue resume.
194
+ // Destruction is handled by idle timeout / dismiss / error in agent-spawner.
195
+ proxy.once("exit", () => {
196
+ stopMcpBridge(sessionId).catch(() => { });
197
+ vmLog.dim("docker-connector", `Workflow agent exited — container ${sessionId} kept alive for idle window`, sessionId);
198
+ });
199
+ }
200
+ return {
201
+ pid: proxy.pid,
202
+ process: proxy,
203
+ vmId: sessionId,
204
+ };
205
+ },
206
+ async stop(entry) {
207
+ await stopAgentProcess(entry);
208
+ if (entry.vmId) {
209
+ const sm = getVmManager();
210
+ await sm.destroySession(entry.taskId);
211
+ }
212
+ },
213
+ async resume(params, ctx) {
214
+ const sm = getVmManager();
215
+ const session = sm.getSessionByTaskId(params.taskId);
216
+ if (session && session.status === "RUNNING") {
217
+ vmLog.cyan("docker-connector", `Resuming in existing container ${session.id}`, session.id);
218
+ // Restart MCP bridge (stopped on previous agent exit)
219
+ await startSessionMcpBridge(session, params);
220
+ // Use --continue for workflow resume in existing container (full context)
221
+ if (params.workflowContext) {
222
+ const continuePrompt = params.prompt ??
223
+ "Continue your work from where you left off. Do not repeat completed work.";
224
+ const agentArgs = buildContinueArgs("/session/workspace/.claude/mcp_config.json", continuePrompt);
225
+ const proxy = await sm.spawnAgent(params.taskId, "claude", agentArgs);
226
+ // Keep container alive for next idle window cycle
227
+ proxy.once("exit", () => {
228
+ stopMcpBridge(session.id).catch(() => { });
229
+ vmLog.dim("docker-connector", `Resumed workflow agent exited — container ${session.id} kept alive`, session.id);
230
+ });
231
+ return {
232
+ pid: proxy.pid,
233
+ process: proxy,
234
+ vmId: session.id,
235
+ };
236
+ }
237
+ // Board task resume: use --continue with user's answer (container alive from ask-human)
238
+ const resumePrompt = ctx?.response ?? "Continue your work.";
239
+ await startSessionMcpBridge(session, params);
240
+ const agentArgs = buildContinueArgs("/session/workspace/.claude/mcp_config.json", resumePrompt);
241
+ const proxy = await sm.spawnAgent(params.taskId, "claude", agentArgs);
242
+ return {
243
+ pid: proxy.pid,
244
+ process: proxy,
245
+ vmId: session.id,
246
+ };
247
+ }
248
+ // No existing session — check for snapshot restore
249
+ if (params.sessionSnapshotBase64 && this.resumeWithSnapshot) {
250
+ return this.resumeWithSnapshot(params);
251
+ }
252
+ // Fallback: full fresh start with context in prompt
253
+ const basePrompt = params.prompt ?? buildDefaultPrompt(params.skillName);
254
+ return this.start({
255
+ ...params,
256
+ prompt: buildResumePrompt(basePrompt, ctx),
257
+ });
258
+ },
259
+ /**
260
+ * Resume a workflow step using a restored session snapshot.
261
+ * The snapshot has already been restored into the container by the caller.
262
+ * Uses `claude --continue` for full context restoration.
263
+ */
264
+ async resumeWithSnapshot(params) {
265
+ const sm = getVmManager();
266
+ // 1. Ensure Docker is ready + image available
267
+ await sm.ensureVmRunning();
268
+ // 2. Create session (container + host dirs)
269
+ const sessionId = await sm.createSession(params.taskId);
270
+ const session = sm.getSessionByTaskId(params.taskId);
271
+ // 3. Clone repo (picks up auto-saved code from git push)
272
+ await sm.cloneRepo(sessionId, params.repoUrl, params.taskBranch, params.workflowBaseBranch);
273
+ // 4. Prepare workspace files
274
+ const skillFiles = [];
275
+ if (params.skill) {
276
+ for (const file of params.skill.files) {
277
+ skillFiles.push({
278
+ relativePath: `${params.skill.name}/${file.path}`,
279
+ content: file.content,
280
+ });
281
+ }
282
+ }
283
+ sm.prepareWorkspace(sessionId, {
284
+ contextJson: JSON.stringify(params.taskContext ?? {}, null, 2),
285
+ mcpConfigJson: buildMcpConfigJson(session.sessionIndex, session.mcpSecret, await resolveMcpRelayHost()),
286
+ skillFiles,
287
+ });
288
+ await sm.exec(params.taskId, "bash", [
289
+ "-c",
290
+ [
291
+ "mkdir -p /session/workspace/.forgeai /session/workspace/.claude",
292
+ "cp /session/context.json /session/workspace/.forgeai/context.json",
293
+ "cp /session/mcp_config.json /session/workspace/.claude/mcp_config.json",
294
+ ].join(" && "),
295
+ ]);
296
+ if (skillFiles.length > 0) {
297
+ await sm.exec(params.taskId, "bash", [
298
+ "-c",
299
+ [
300
+ "mkdir -p /session/workspace/.claude/skills",
301
+ "cp -r /session/skills/. /session/workspace/.claude/skills/",
302
+ ].join(" && "),
303
+ ]);
304
+ }
305
+ // 5. Restore the session snapshot (already written to sessionDir by caller)
306
+ const { restoreSessionSnapshot } = await import("../../vm/session-snapshot.js");
307
+ const snapshotRestored = await restoreSessionSnapshot(session, params.sessionSnapshotBase64);
308
+ // 6. Start MCP bridge
309
+ await startSessionMcpBridge(session, params);
310
+ // 7. Spawn with --continue if snapshot restored, otherwise -p prompt
311
+ let proxy;
312
+ if (snapshotRestored) {
313
+ vmLog.cyan("docker-connector", `Spawning with --continue (snapshot restored) in ${sessionId}`, sessionId);
314
+ const continuePrompt = params.prompt ??
315
+ "Continue your work from where you left off. Do not repeat completed work.";
316
+ const agentArgs = buildContinueArgs("/session/workspace/.claude/mcp_config.json", continuePrompt);
317
+ proxy = await sm.spawnAgent(params.taskId, "claude", agentArgs);
318
+ }
319
+ else {
320
+ vmLog.warn("docker-connector", `Snapshot restore failed — falling back to prompt in ${sessionId}`, sessionId);
321
+ const prompt = params.prompt ?? buildDefaultPrompt(params.skillName);
322
+ const agentArgs = buildAgentArgs("/session/workspace/.claude/mcp_config.json", prompt);
323
+ proxy = await sm.spawnAgent(params.taskId, "claude", agentArgs);
324
+ }
325
+ // Workflow: keep container alive for idle window
326
+ proxy.once("exit", () => {
327
+ stopMcpBridge(sessionId).catch(() => { });
328
+ vmLog.dim("docker-connector", `Snapshot-resumed workflow agent exited — container ${sessionId} kept alive`, sessionId);
329
+ });
330
+ return {
331
+ pid: proxy.pid,
332
+ process: proxy,
333
+ vmId: sessionId,
334
+ };
335
+ },
336
+ };
337
+ //# sourceMappingURL=docker-connector.js.map
@@ -0,0 +1,11 @@
1
+ /**
2
+ * AgentConnector implementation that runs agents inside bubblewrap sandboxes
3
+ * within the shared native VM (Virtualization.framework via Swift N-API addon).
4
+ *
5
+ * Replaces the old vm-connector that used one VM per session with SSH streaming.
6
+ * This connector uses vsock for all communication and shares a single VM across
7
+ * all concurrent agent sessions.
8
+ */
9
+ import type { AgentConnector } from "./connector.js";
10
+ export declare const nativeVmConnector: AgentConnector;
11
+ //# sourceMappingURL=native-vm-connector.d.ts.map