@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,4 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { McpSessionContext } from "../types.js";
3
+ export declare function registerForgeRemoveGroupDependency(server: McpServer, context: McpSessionContext): void;
4
+ //# sourceMappingURL=forge-remove-group-dependency.d.ts.map
@@ -0,0 +1,19 @@
1
+ import { ForgeRemoveGroupDependencyInputSchema } from "@getforgeai/protocol";
2
+ import { executeSocketEmit } from "../mcp-response.js";
3
+ const shape = ForgeRemoveGroupDependencyInputSchema.shape;
4
+ export function registerForgeRemoveGroupDependency(server, context) {
5
+ server.registerTool("forge_remove_group_dependency", {
6
+ description: "Remove a dependency between two task groups. References groups by name (most recently created match).",
7
+ inputSchema: {
8
+ groupName: shape.groupName.describe("Name of the blocked group (the one that depends on the other)"),
9
+ dependsOnGroupName: shape.dependsOnGroupName.describe("Name of the blocking group (the one it depends on)"),
10
+ },
11
+ }, async (params) => {
12
+ return executeSocketEmit(context, "remove group dependency", () => context.socketClient.emitMcpTaskGroupRemoveDependency({
13
+ projectId: context.projectId,
14
+ groupName: params.groupName,
15
+ dependsOnGroupName: params.dependsOnGroupName,
16
+ }));
17
+ });
18
+ }
19
+ //# sourceMappingURL=forge-remove-group-dependency.js.map
@@ -0,0 +1,4 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { McpSessionContext } from "../types.js";
3
+ export declare function registerForgeRemoveTaskDependency(server: McpServer, context: McpSessionContext): void;
4
+ //# sourceMappingURL=forge-remove-task-dependency.d.ts.map
@@ -0,0 +1,19 @@
1
+ import { ForgeRemoveTaskDependencyInputSchema } from "@getforgeai/protocol";
2
+ import { executeSocketEmit } from "../mcp-response.js";
3
+ const shape = ForgeRemoveTaskDependencyInputSchema.shape;
4
+ export function registerForgeRemoveTaskDependency(server, context) {
5
+ server.registerTool("forge_remove_task_dependency", {
6
+ description: "Remove a dependency between two tasks. References tasks by title (most recently created match).",
7
+ inputSchema: {
8
+ taskName: shape.taskName.describe("Title of the blocked task (the one that depends on the other)"),
9
+ dependsOnTaskName: shape.dependsOnTaskName.describe("Title of the blocking task (the one it depends on)"),
10
+ },
11
+ }, async (params) => {
12
+ return executeSocketEmit(context, "remove task dependency", () => context.socketClient.emitMcpTaskRemoveDependency({
13
+ projectId: context.projectId,
14
+ taskName: params.taskName,
15
+ dependsOnTaskName: params.dependsOnTaskName,
16
+ }));
17
+ });
18
+ }
19
+ //# sourceMappingURL=forge-remove-task-dependency.js.map
@@ -0,0 +1,4 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { McpSessionContext } from "../types.js";
3
+ export declare function registerForgeRequeue(server: McpServer, context: McpSessionContext): void;
4
+ //# sourceMappingURL=forge-requeue.d.ts.map
@@ -0,0 +1,39 @@
1
+ import { z } from "zod";
2
+ import { mcpSuccess, mcpError, handleRestError } from "../mcp-response.js";
3
+ export function registerForgeRequeue(server, context) {
4
+ server.registerTool("forge_requeue", {
5
+ description: "Re-queue the current task to be re-dispatched on the same column. Returns { allowed, count, max }. If allowed=false (max reached), decide: call forge_ask_human or skip to next gate.",
6
+ inputSchema: {
7
+ taskId: z.string().min(1).describe("Task ID to re-queue"),
8
+ reason: z
9
+ .string()
10
+ .optional()
11
+ .describe("Optional reason for re-queue (e.g., review feedback)"),
12
+ },
13
+ }, async ({ taskId, reason }) => {
14
+ // Check current task status to reject if still in dispatch queue
15
+ let currentTask;
16
+ try {
17
+ currentTask = await context.restClient.getTask(taskId);
18
+ }
19
+ catch (error) {
20
+ return handleRestError(error, taskId);
21
+ }
22
+ // Reject if not yet dispatched — task is already in the queue
23
+ if (!currentTask.kanbanSubStatus) {
24
+ return mcpError({
25
+ error: "INVALID_STATUS",
26
+ message: "Task has not been dispatched yet. It is already in the dispatch queue.",
27
+ });
28
+ }
29
+ // Call REST endpoint for requeue
30
+ try {
31
+ const result = await context.restClient.requeueTask(taskId, reason);
32
+ return mcpSuccess(result);
33
+ }
34
+ catch (error) {
35
+ return handleRestError(error, taskId);
36
+ }
37
+ });
38
+ }
39
+ //# sourceMappingURL=forge-requeue.js.map
@@ -0,0 +1,4 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { McpSessionContext } from "../types.js";
3
+ export declare function registerForgeSubmitPr(server: McpServer, context: McpSessionContext): void;
4
+ //# sourceMappingURL=forge-submit-pr.d.ts.map
@@ -0,0 +1,68 @@
1
+ import { z } from "zod";
2
+ import { MCP_ERROR_CODE } from "../types.js";
3
+ export function registerForgeSubmitPr(server, context) {
4
+ server.tool("forge_submit_pr", "Register a pull request on the ForgeAI board. Cloud tracks the PR and returns a tracking ID.", {
5
+ taskId: z.string().min(1).describe("Task ID the PR belongs to"),
6
+ prTitle: z.string().min(1).describe("Pull request title"),
7
+ baseBranch: z
8
+ .string()
9
+ .min(1)
10
+ .describe("Base branch for the pull request"),
11
+ description: z.string().min(1).describe("Pull request description"),
12
+ }, async ({ taskId, prTitle, baseBranch, description }) => {
13
+ try {
14
+ if (!context.socketClient.socket.connected) {
15
+ return {
16
+ isError: true,
17
+ content: [
18
+ {
19
+ type: "text",
20
+ text: JSON.stringify({
21
+ error: MCP_ERROR_CODE.NETWORK_ERROR,
22
+ message: "Socket.io not connected. Cannot submit PR.",
23
+ retryHint: "Wait for reconnection and retry.",
24
+ }),
25
+ },
26
+ ],
27
+ };
28
+ }
29
+ context.socketClient.emitTaskSubmitPr({
30
+ taskId,
31
+ prTitle,
32
+ baseBranch,
33
+ description,
34
+ projectId: context.projectId,
35
+ });
36
+ // Generate a tracking ID (Cloud will associate this with the task)
37
+ const prTrackingId = `pr-${taskId}-${Date.now()}`;
38
+ return {
39
+ content: [
40
+ {
41
+ type: "text",
42
+ text: JSON.stringify({
43
+ taskId,
44
+ prTrackingId,
45
+ success: true,
46
+ }),
47
+ },
48
+ ],
49
+ };
50
+ }
51
+ catch (error) {
52
+ return {
53
+ isError: true,
54
+ content: [
55
+ {
56
+ type: "text",
57
+ text: JSON.stringify({
58
+ error: MCP_ERROR_CODE.NETWORK_ERROR,
59
+ message: error instanceof Error ? error.message : String(error),
60
+ retryHint: "Retry in a few seconds.",
61
+ }),
62
+ },
63
+ ],
64
+ };
65
+ }
66
+ });
67
+ }
68
+ //# sourceMappingURL=forge-submit-pr.js.map
@@ -0,0 +1,4 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { McpSessionContext } from "../types.js";
3
+ export declare function registerForgeUpdateStatus(server: McpServer, context: McpSessionContext): void;
4
+ //# sourceMappingURL=forge-update-status.d.ts.map
@@ -0,0 +1,77 @@
1
+ import { z } from "zod";
2
+ import { KANBAN_SUB_STATUS } from "@getforgeai/protocol";
3
+ import { mcpSuccess, mcpError, mcpNetworkError, handleRestError, } from "../mcp-response.js";
4
+ /** Valid kanbanSubStatus transitions (from → Set of valid targets) */
5
+ const VALID_TRANSITIONS = {
6
+ WAITING: ["ACTIVE"],
7
+ ACTIVE: ["DONE", "AWAITING_HUMAN"],
8
+ AWAITING_HUMAN: ["ACTIVE"],
9
+ PENDING_CORRECTION: ["ACTIVE"],
10
+ DONE: [],
11
+ };
12
+ export function registerForgeUpdateStatus(server, context) {
13
+ server.registerTool("forge_update_status", {
14
+ description: "Update a task's status in ForgeAI. Validates against the task state machine before applying.",
15
+ inputSchema: {
16
+ taskId: z.string().min(1).describe("Task ID to update"),
17
+ kanbanSubStatus: z
18
+ .enum(KANBAN_SUB_STATUS)
19
+ .describe("New kanban sub-status to set"),
20
+ },
21
+ }, async ({ taskId, kanbanSubStatus: newSubStatus }) => {
22
+ // Fetch current task to validate transition
23
+ let currentTask;
24
+ try {
25
+ currentTask = await context.restClient.getTask(taskId);
26
+ }
27
+ catch (error) {
28
+ return handleRestError(error, taskId);
29
+ }
30
+ // Validate state transition
31
+ const currentSubStatus = currentTask.kanbanSubStatus ?? "WAITING";
32
+ const validTargets = VALID_TRANSITIONS[currentSubStatus];
33
+ if (!validTargets?.includes(newSubStatus)) {
34
+ return mcpError({
35
+ error: "INVALID_STATUS",
36
+ message: `Cannot transition from "${currentSubStatus}" to "${newSubStatus}"`,
37
+ validTransitions: validTargets ?? [],
38
+ });
39
+ }
40
+ // Try Socket.io first for real-time propagation
41
+ try {
42
+ if (context.socketClient.socket.connected) {
43
+ context.socketClient.emitTaskUpdate({
44
+ taskId,
45
+ kanbanSubStatus: newSubStatus,
46
+ projectId: context.projectId,
47
+ });
48
+ return mcpSuccess({
49
+ taskId,
50
+ previousSubStatus: currentSubStatus,
51
+ newSubStatus,
52
+ success: true,
53
+ });
54
+ }
55
+ }
56
+ catch {
57
+ // Socket.io failed, fall through to REST
58
+ }
59
+ // Fallback to REST
60
+ try {
61
+ await context.restClient.updateTaskSubStatus({
62
+ taskId,
63
+ kanbanSubStatus: newSubStatus,
64
+ });
65
+ return mcpSuccess({
66
+ taskId,
67
+ previousSubStatus: currentSubStatus,
68
+ newSubStatus,
69
+ success: true,
70
+ });
71
+ }
72
+ catch (error) {
73
+ return mcpNetworkError(error, "Both Socket.io and REST failed. Retry in a few seconds.");
74
+ }
75
+ });
76
+ }
77
+ //# sourceMappingURL=forge-update-status.js.map
@@ -0,0 +1,4 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { McpSessionContext } from "../types.js";
3
+ export declare function registerForgeUpdateTaskGroup(server: McpServer, context: McpSessionContext): void;
4
+ //# sourceMappingURL=forge-update-task-group.d.ts.map
@@ -0,0 +1,21 @@
1
+ import { ForgeUpdateTaskGroupInputSchema } from "@getforgeai/protocol";
2
+ import { executeSocketEmit } from "../mcp-response.js";
3
+ const shape = ForgeUpdateTaskGroupInputSchema.shape;
4
+ export function registerForgeUpdateTaskGroup(server, context) {
5
+ server.registerTool("forge_update_task_group", {
6
+ description: "Update an existing task group by name (most recently created match). Can update name and/or description.",
7
+ inputSchema: {
8
+ name: shape.name.describe("Current group name to find"),
9
+ newName: shape.newName.describe("New name for the group"),
10
+ description: shape.description.describe("New description"),
11
+ },
12
+ }, async (params) => {
13
+ return executeSocketEmit(context, "update task group", () => context.socketClient.emitMcpTaskGroupUpdate({
14
+ projectId: context.projectId,
15
+ name: params.name,
16
+ newName: params.newName,
17
+ description: params.description,
18
+ }));
19
+ });
20
+ }
21
+ //# sourceMappingURL=forge-update-task-group.js.map
@@ -0,0 +1,4 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { McpSessionContext } from "../types.js";
3
+ export declare function registerForgeUpdateTask(server: McpServer, context: McpSessionContext): void;
4
+ //# sourceMappingURL=forge-update-task.d.ts.map
@@ -0,0 +1,25 @@
1
+ import { ForgeUpdateTaskInputSchema } from "@getforgeai/protocol";
2
+ import { executeSocketEmit } from "../mcp-response.js";
3
+ const shape = ForgeUpdateTaskInputSchema.shape;
4
+ export function registerForgeUpdateTask(server, context) {
5
+ server.registerTool("forge_update_task", {
6
+ description: "Update an existing task by name (most recently created match). Can update title, description, priority, and/or storyPoints.",
7
+ inputSchema: {
8
+ name: shape.name.describe("Current task title to find"),
9
+ title: shape.title.describe("New title for the task"),
10
+ description: shape.description.describe("New description"),
11
+ priority: shape.priority.describe("New priority"),
12
+ storyPoints: shape.storyPoints.describe("New story points estimate"),
13
+ },
14
+ }, async (params) => {
15
+ return executeSocketEmit(context, "update task", () => context.socketClient.emitMcpTaskUpdate({
16
+ projectId: context.projectId,
17
+ name: params.name,
18
+ title: params.title,
19
+ description: params.description,
20
+ priority: params.priority,
21
+ storyPoints: params.storyPoints,
22
+ }));
23
+ });
24
+ }
25
+ //# sourceMappingURL=forge-update-task.js.map
@@ -0,0 +1,4 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { McpSessionContext } from "../types.js";
3
+ export declare function registerForgeValidateMerge(server: McpServer, _context: McpSessionContext): void;
4
+ //# sourceMappingURL=forge-validate-merge.d.ts.map
@@ -0,0 +1,194 @@
1
+ import { execFile } from "node:child_process";
2
+ import { readFile } from "node:fs/promises";
3
+ import { resolve } from "node:path";
4
+ import { promisify } from "node:util";
5
+ import { z } from "zod";
6
+ import { mcpSuccess, mcpError } from "../mcp-response.js";
7
+ import { MCP_ERROR_CODE } from "../types.js";
8
+ const execFileAsync = promisify(execFile);
9
+ /** Maximum output bytes to include in command results. */
10
+ const MAX_OUTPUT_BYTES = 50 * 1024; // 50 KB
11
+ /** Timeout for build/test commands (10 minutes). */
12
+ const COMMAND_TIMEOUT_MS = 10 * 60 * 1000;
13
+ function truncate(text, maxBytes) {
14
+ if (text.length <= maxBytes)
15
+ return text;
16
+ return text.slice(0, maxBytes) + "\n\n--- truncated ---";
17
+ }
18
+ /**
19
+ * Run a shell command in the given directory and return stdout + stderr.
20
+ * Returns the result instead of throwing, so callers can inspect output on failure.
21
+ */
22
+ async function runCommand(cwd, command, args) {
23
+ try {
24
+ const { stdout, stderr } = await execFileAsync(command, args, {
25
+ cwd,
26
+ timeout: COMMAND_TIMEOUT_MS,
27
+ env: { ...process.env, CI: "true", NODE_ENV: "test" },
28
+ });
29
+ return { success: true, stdout: stdout.trim(), stderr: stderr.trim() };
30
+ }
31
+ catch (error) {
32
+ const execError = error;
33
+ return {
34
+ success: false,
35
+ stdout: (execError.stdout ?? "").trim(),
36
+ stderr: (execError.stderr ?? execError.message ?? "").trim(),
37
+ };
38
+ }
39
+ }
40
+ /**
41
+ * Detect available package manager and test command from package.json.
42
+ */
43
+ async function detectCommands(repoPath) {
44
+ const defaults = {
45
+ packageManager: "pnpm",
46
+ installArgs: ["install", "--frozen-lockfile"],
47
+ buildArgs: ["build"],
48
+ testArgs: ["test:ci"],
49
+ hasPackageJson: false,
50
+ };
51
+ try {
52
+ const packageJsonPath = resolve(repoPath, "package.json");
53
+ const raw = await readFile(packageJsonPath, "utf-8");
54
+ const pkg = JSON.parse(raw);
55
+ defaults.hasPackageJson = true;
56
+ // Detect package manager from packageManager field or lockfile
57
+ if (pkg.packageManager?.startsWith("yarn")) {
58
+ defaults.packageManager = "yarn";
59
+ defaults.installArgs = ["install", "--frozen-lockfile"];
60
+ }
61
+ else if (pkg.packageManager?.startsWith("npm")) {
62
+ defaults.packageManager = "npm";
63
+ defaults.installArgs = ["ci"];
64
+ }
65
+ // Default: pnpm
66
+ // Detect test command
67
+ const scripts = pkg.scripts ?? {};
68
+ if (scripts["test:ci"]) {
69
+ defaults.testArgs = ["test:ci"];
70
+ }
71
+ else if (scripts["test"]) {
72
+ defaults.testArgs = ["test"];
73
+ }
74
+ else {
75
+ defaults.testArgs = [];
76
+ }
77
+ // Detect build command
78
+ if (!scripts["build"]) {
79
+ defaults.buildArgs = [];
80
+ }
81
+ return defaults;
82
+ }
83
+ catch {
84
+ return defaults;
85
+ }
86
+ }
87
+ export function registerForgeValidateMerge(server, _context) {
88
+ server.registerTool("forge_validate_merge", {
89
+ description: "Validate a merge by running install, build, and tests in the repository. Detects the package manager and test command from package.json. Returns structured results for each step (pass/fail + output).",
90
+ inputSchema: {
91
+ repoPath: z
92
+ .string()
93
+ .min(1)
94
+ .describe("Absolute path to the git repository to validate"),
95
+ },
96
+ }, async ({ repoPath }) => {
97
+ try {
98
+ const config = await detectCommands(repoPath);
99
+ if (!config.hasPackageJson) {
100
+ return mcpError({
101
+ error: MCP_ERROR_CODE.VALIDATION_ERROR,
102
+ message: `No package.json found in "${repoPath}". Cannot run validation.`,
103
+ hint: "Ensure the repoPath points to a valid Node.js project root.",
104
+ });
105
+ }
106
+ const results = [];
107
+ // 1. Install dependencies
108
+ const installResult = await runCommand(repoPath, config.packageManager, config.installArgs);
109
+ results.push({
110
+ step: "install",
111
+ success: installResult.success,
112
+ stdout: truncate(installResult.stdout, MAX_OUTPUT_BYTES),
113
+ stderr: truncate(installResult.stderr, MAX_OUTPUT_BYTES),
114
+ });
115
+ if (!installResult.success) {
116
+ return mcpSuccess({
117
+ success: false,
118
+ failedStep: "install",
119
+ message: `Dependency installation failed. Build and tests were skipped.`,
120
+ results,
121
+ });
122
+ }
123
+ // 2. Build
124
+ if (config.buildArgs.length > 0) {
125
+ const buildResult = await runCommand(repoPath, config.packageManager, config.buildArgs);
126
+ results.push({
127
+ step: "build",
128
+ success: buildResult.success,
129
+ stdout: truncate(buildResult.stdout, MAX_OUTPUT_BYTES),
130
+ stderr: truncate(buildResult.stderr, MAX_OUTPUT_BYTES),
131
+ });
132
+ if (!buildResult.success) {
133
+ return mcpSuccess({
134
+ success: false,
135
+ failedStep: "build",
136
+ message: `Build failed. Tests were skipped.`,
137
+ results,
138
+ });
139
+ }
140
+ }
141
+ else {
142
+ results.push({
143
+ step: "build",
144
+ success: true,
145
+ stdout: "",
146
+ stderr: "",
147
+ skipped: true,
148
+ });
149
+ }
150
+ // 3. Tests
151
+ if (config.testArgs.length > 0) {
152
+ const testResult = await runCommand(repoPath, config.packageManager, config.testArgs);
153
+ results.push({
154
+ step: "test",
155
+ success: testResult.success,
156
+ stdout: truncate(testResult.stdout, MAX_OUTPUT_BYTES),
157
+ stderr: truncate(testResult.stderr, MAX_OUTPUT_BYTES),
158
+ });
159
+ if (!testResult.success) {
160
+ return mcpSuccess({
161
+ success: false,
162
+ failedStep: "test",
163
+ message: `Tests failed.`,
164
+ results,
165
+ });
166
+ }
167
+ }
168
+ else {
169
+ results.push({
170
+ step: "test",
171
+ success: true,
172
+ stdout: "",
173
+ stderr: "",
174
+ skipped: true,
175
+ });
176
+ }
177
+ // All steps passed
178
+ return mcpSuccess({
179
+ success: true,
180
+ message: `Validation passed: install, build, and tests all succeeded.`,
181
+ results,
182
+ });
183
+ }
184
+ catch (error) {
185
+ const message = error instanceof Error ? error.message : String(error);
186
+ return mcpError({
187
+ error: MCP_ERROR_CODE.NETWORK_ERROR,
188
+ message: `Unexpected error during validation: ${message}`,
189
+ hint: "Check that the repository path is valid and accessible.",
190
+ });
191
+ }
192
+ });
193
+ }
194
+ //# sourceMappingURL=forge-validate-merge.js.map
@@ -0,0 +1,39 @@
1
+ import type { createRestClient } from "../lib/rest-client.js";
2
+ import type { createSocketClient } from "../lib/socket-client.js";
3
+ /**
4
+ * Session context provided to each MCP server instance.
5
+ * Contains references to the daemon's existing clients for Cloud relay.
6
+ */
7
+ export type McpSessionContext = {
8
+ taskId: string;
9
+ projectId: string;
10
+ orgId: string;
11
+ restClient: ReturnType<typeof createRestClient>;
12
+ socketClient: ReturnType<typeof createSocketClient>;
13
+ /** Current task state — updated by resource reads */
14
+ currentTask: McpTaskContext | null;
15
+ /** Project root path for reading local files (CLAUDE.md, etc.) */
16
+ projectRoot: string | null;
17
+ /** Workflow context — set when MCP server runs in a workflow step context (Story 6b.4) */
18
+ workflowId?: string;
19
+ stepId?: string;
20
+ };
21
+ /** Task context served by forge://task/current resource */
22
+ export type McpTaskContext = {
23
+ id: string;
24
+ title: string;
25
+ description: string;
26
+ status: string;
27
+ kanbanColumn?: string | null;
28
+ assigneeId?: string | null;
29
+ cliDeviceId?: string | null;
30
+ };
31
+ /** MCP error codes returned to agents */
32
+ export declare const MCP_ERROR_CODE: {
33
+ readonly TASK_NOT_FOUND: "TASK_NOT_FOUND";
34
+ readonly NETWORK_ERROR: "NETWORK_ERROR";
35
+ readonly INVALID_STATUS: "INVALID_STATUS";
36
+ readonly VALIDATION_ERROR: "VALIDATION_ERROR";
37
+ };
38
+ export type McpErrorCode = (typeof MCP_ERROR_CODE)[keyof typeof MCP_ERROR_CODE];
39
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,8 @@
1
+ /** MCP error codes returned to agents */
2
+ export const MCP_ERROR_CODE = {
3
+ TASK_NOT_FOUND: "TASK_NOT_FOUND",
4
+ NETWORK_ERROR: "NETWORK_ERROR",
5
+ INVALID_STATUS: "INVALID_STATUS",
6
+ VALIDATION_ERROR: "VALIDATION_ERROR",
7
+ };
8
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,14 @@
1
+ import type { Socket } from "socket.io-client";
2
+ import type { ClientToServerEvents, ServerToClientEvents } from "@getforgeai/protocol";
3
+ type TypedClientSocket = Socket<ServerToClientEvents, ClientToServerEvents>;
4
+ export type ArtifactHandlerOptions = {
5
+ artifactDir?: string;
6
+ };
7
+ /**
8
+ * Register artifact event handlers on a CLI socket connection.
9
+ * Listens for `workflow:artifact` events from Cloud, writes artifact
10
+ * content to the Git working tree, and emits confirmation back.
11
+ */
12
+ export declare function registerArtifactHandlers(socket: TypedClientSocket, orgId: string, options?: ArtifactHandlerOptions): void;
13
+ export {};
14
+ //# sourceMappingURL=artifact-manager.d.ts.map
@@ -0,0 +1,55 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { basename, dirname, join } from "node:path";
3
+ import chalk from "chalk";
4
+ const DEFAULT_ARTIFACT_DIR = "docs/methodology";
5
+ /**
6
+ * Register artifact event handlers on a CLI socket connection.
7
+ * Listens for `workflow:artifact` events from Cloud, writes artifact
8
+ * content to the Git working tree, and emits confirmation back.
9
+ */
10
+ export function registerArtifactHandlers(socket, orgId, options = {}) {
11
+ const artifactDir = options.artifactDir ?? DEFAULT_ARTIFACT_DIR;
12
+ socket.on("workflow:artifact", (payload) => {
13
+ if (!payload.content) {
14
+ console.warn(chalk.yellow("[artifact] Received artifact event without content"));
15
+ return;
16
+ }
17
+ // Sanitize fileName to prevent path traversal (L1)
18
+ const rawFileName = payload.fileName || `${payload.stepKey}.md`;
19
+ const fileName = basename(rawFileName);
20
+ const filePath = join(artifactDir, fileName);
21
+ const absolutePath = join(process.cwd(), filePath);
22
+ try {
23
+ const dir = dirname(absolutePath);
24
+ if (!existsSync(dir)) {
25
+ mkdirSync(dir, { recursive: true });
26
+ }
27
+ writeFileSync(absolutePath, payload.content, "utf-8");
28
+ console.log(chalk.green(`[artifact] Written: ${filePath}`));
29
+ socket.emit("workflow:artifact", {
30
+ orgId,
31
+ projectId: payload.projectId,
32
+ workflowId: payload.workflowId,
33
+ stepId: payload.stepId,
34
+ fileName,
35
+ filePath,
36
+ artifactType: payload.artifactType,
37
+ stepKey: payload.stepKey,
38
+ timestamp: new Date().toISOString(),
39
+ });
40
+ }
41
+ catch (error) {
42
+ const message = error instanceof Error ? error.message : String(error);
43
+ console.error(chalk.red(`[artifact] Failed to write ${filePath}: ${message}`));
44
+ // Emit error event so Cloud/UI can handle the failure (H1)
45
+ socket.emit("workflow:artifact:error", {
46
+ orgId,
47
+ workflowId: payload.workflowId,
48
+ stepId: payload.stepId,
49
+ error: message,
50
+ timestamp: new Date().toISOString(),
51
+ });
52
+ }
53
+ });
54
+ }
55
+ //# sourceMappingURL=artifact-manager.js.map