ralph-mcp 1.0.0

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.
@@ -0,0 +1,137 @@
1
+ import { readFileSync } from "fs";
2
+ import matter from "gray-matter";
3
+ /**
4
+ * Parse a PRD markdown file into structured data.
5
+ * Supports both markdown format and JSON format.
6
+ */
7
+ export function parsePrdFile(filePath) {
8
+ const content = readFileSync(filePath, "utf-8");
9
+ // Check if it's JSON format
10
+ if (filePath.endsWith(".json")) {
11
+ return parsePrdJson(content);
12
+ }
13
+ return parsePrdMarkdown(content);
14
+ }
15
+ function parsePrdJson(content) {
16
+ const data = JSON.parse(content);
17
+ return {
18
+ title: data.description || data.title || "Untitled PRD",
19
+ description: data.description || "",
20
+ branchName: data.branchName || "ralph/unnamed",
21
+ userStories: (data.userStories || []).map((us, index) => ({
22
+ id: us.id || `US-${String(index + 1).padStart(3, "0")}`,
23
+ title: us.title || "",
24
+ description: us.description || "",
25
+ acceptanceCriteria: us.acceptanceCriteria || [],
26
+ priority: us.priority || index + 1,
27
+ })),
28
+ };
29
+ }
30
+ function parsePrdMarkdown(content) {
31
+ // Normalize line endings (CRLF -> LF)
32
+ content = content.replace(/\r\n/g, "\n");
33
+ const { data: frontmatter, content: body } = matter(content);
34
+ // Extract title from first H1 or frontmatter
35
+ const titleMatch = body.match(/^#\s+(.+)$/m);
36
+ const title = frontmatter.title || titleMatch?.[1] || "Untitled PRD";
37
+ // Extract branch name from frontmatter or generate from title
38
+ const branchName = frontmatter.branch ||
39
+ `ralph/${title.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`;
40
+ // Extract description
41
+ const descMatch = body.match(/##\s*(?:Description|描述|Overview|概述)\s*\n([\s\S]*?)(?=\n##|\n$)/i);
42
+ const description = descMatch?.[1]?.trim() || title;
43
+ // Extract user stories
44
+ const userStories = extractUserStories(body);
45
+ return {
46
+ title,
47
+ description,
48
+ branchName,
49
+ userStories,
50
+ };
51
+ }
52
+ function extractUserStories(content) {
53
+ const stories = [];
54
+ // First, try to find the User Stories section
55
+ const userStoriesSection = content.match(/##\s*(?:User Stories|用户故事)\s*\n([\s\S]*?)(?=\n##[^#]|$)/i);
56
+ // Add leading newline to ensure pattern matches at start
57
+ const searchContent = userStoriesSection?.[1]
58
+ ? "\n" + userStoriesSection[1]
59
+ : content;
60
+ // Pattern 1: ### US-XXX: Title format (supports US-1, US-01, US-001)
61
+ const usPattern = /\n###\s*(US-\d+)[:\s]+(.+?)\n([\s\S]*?)(?=\n###\s*US-|\n##[^#]|$)/gi;
62
+ let match;
63
+ while ((match = usPattern.exec(searchContent)) !== null) {
64
+ const [, id, title, body] = match;
65
+ const story = parseUserStoryBody(id.toUpperCase(), title, body);
66
+ story.priority = stories.length + 1;
67
+ stories.push(story);
68
+ }
69
+ // Pattern 2: Numbered list with checkboxes (only if no US-XXX found)
70
+ if (stories.length === 0) {
71
+ const listPattern = /^\d+\.\s*\[[ x]\]\s*\*\*(.+?)\*\*[:\s]*([\s\S]*?)(?=\n\d+\.\s*\[|\n##|$)/gim;
72
+ let index = 0;
73
+ while ((match = listPattern.exec(searchContent)) !== null) {
74
+ const [, title, body] = match;
75
+ index++;
76
+ const id = `US-${String(index).padStart(3, "0")}`;
77
+ const story = parseUserStoryBody(id, title.trim(), body);
78
+ stories.push(story);
79
+ }
80
+ }
81
+ // Pattern 3: Simple numbered list (only in User Stories section, not entire content)
82
+ if (stories.length === 0 && userStoriesSection) {
83
+ const simplePattern = /^\d+\.\s*(.+?)(?:\n|$)/gm;
84
+ let index = 0;
85
+ while ((match = simplePattern.exec(userStoriesSection[1])) !== null) {
86
+ const [, title] = match;
87
+ if (title.trim() && !title.startsWith("#")) {
88
+ index++;
89
+ stories.push({
90
+ id: `US-${String(index).padStart(3, "0")}`,
91
+ title: title.trim(),
92
+ description: "",
93
+ acceptanceCriteria: [],
94
+ priority: index,
95
+ });
96
+ }
97
+ }
98
+ }
99
+ return stories;
100
+ }
101
+ function parseUserStoryBody(id, title, body) {
102
+ // Extract description (As a... I want... So that...)
103
+ const descMatch = body.match(/As\s+a[n]?\s+.+?(?:,\s*)?I\s+want.+?(?:,\s*)?So\s+that.+?(?:\.|$)/is);
104
+ const description = descMatch?.[0]?.trim() || "";
105
+ // Extract acceptance criteria
106
+ const acMatch = body.match(/(?:Acceptance\s*Criteria|验收标准|AC)[:\s]*([\s\S]*?)(?=\n(?:Priority|优先级|Notes|备注)|$)/i);
107
+ const acContent = acMatch?.[1] || body;
108
+ const acceptanceCriteria = [];
109
+ const acPattern = /[-*]\s*(.+?)(?:\n|$)/g;
110
+ let acItem;
111
+ while ((acItem = acPattern.exec(acContent)) !== null) {
112
+ const criterion = acItem[1].trim();
113
+ if (criterion && !criterion.startsWith("As a")) {
114
+ acceptanceCriteria.push(criterion);
115
+ }
116
+ }
117
+ // Extract priority
118
+ const priorityMatch = body.match(/(?:Priority|优先级)[:\s]*(\d+)/i);
119
+ const priority = priorityMatch ? parseInt(priorityMatch[1], 10) : 1;
120
+ return {
121
+ id,
122
+ title: title.trim(),
123
+ description,
124
+ acceptanceCriteria,
125
+ priority,
126
+ };
127
+ }
128
+ /**
129
+ * Generate branch name from PRD title
130
+ */
131
+ export function generateBranchName(title) {
132
+ return `ralph/${title
133
+ .toLowerCase()
134
+ .replace(/[^a-z0-9\u4e00-\u9fa5]+/g, "-")
135
+ .replace(/^-|-$/g, "")
136
+ .slice(0, 50)}`;
137
+ }
@@ -0,0 +1,34 @@
1
+ export interface WorktreeInfo {
2
+ path: string;
3
+ branch: string;
4
+ commit: string;
5
+ }
6
+ /**
7
+ * Create a new git worktree for a branch
8
+ */
9
+ export declare function createWorktree(projectRoot: string, branch: string): Promise<string>;
10
+ /**
11
+ * Remove a git worktree
12
+ */
13
+ export declare function removeWorktree(projectRoot: string, worktreePath: string): Promise<void>;
14
+ /**
15
+ * List all worktrees
16
+ */
17
+ export declare function listWorktrees(projectRoot: string): WorktreeInfo[];
18
+ /**
19
+ * Merge a branch into main
20
+ */
21
+ export declare function mergeBranch(projectRoot: string, branch: string, description: string, onConflict?: "auto_theirs" | "auto_ours" | "notify" | "agent"): Promise<{
22
+ success: boolean;
23
+ commitHash?: string;
24
+ hasConflicts: boolean;
25
+ conflictFiles?: string[];
26
+ }>;
27
+ /**
28
+ * Abort a merge in progress
29
+ */
30
+ export declare function abortMerge(projectRoot: string): Promise<void>;
31
+ /**
32
+ * Get list of conflict files
33
+ */
34
+ export declare function getConflictFiles(projectRoot: string): Promise<string[]>;
@@ -0,0 +1,136 @@
1
+ import { execSync, exec } from "child_process";
2
+ import { existsSync } from "fs";
3
+ import { join } from "path";
4
+ import { promisify } from "util";
5
+ const execAsync = promisify(exec);
6
+ /**
7
+ * Create a new git worktree for a branch
8
+ */
9
+ export async function createWorktree(projectRoot, branch) {
10
+ // Extract short name from branch (ralph/task1-agent -> task1-agent)
11
+ const shortName = branch.replace(/^ralph\//, "");
12
+ const worktreePath = join(projectRoot, ".tmp", "worktrees", `ralph-${shortName}`);
13
+ // Check if worktree already exists
14
+ if (existsSync(worktreePath)) {
15
+ console.log(`Worktree already exists at ${worktreePath}`);
16
+ return worktreePath;
17
+ }
18
+ // Check if branch exists
19
+ const branchExists = await checkBranchExists(projectRoot, branch);
20
+ if (branchExists) {
21
+ // Worktree for existing branch
22
+ await execAsync(`git worktree add "${worktreePath}" "${branch}"`, { cwd: projectRoot });
23
+ }
24
+ else {
25
+ // Create new branch from main
26
+ await execAsync(`git worktree add -b "${branch}" "${worktreePath}" main`, { cwd: projectRoot });
27
+ }
28
+ return worktreePath;
29
+ }
30
+ /**
31
+ * Remove a git worktree
32
+ */
33
+ export async function removeWorktree(projectRoot, worktreePath) {
34
+ if (!existsSync(worktreePath)) {
35
+ console.log(`Worktree does not exist at ${worktreePath}`);
36
+ return;
37
+ }
38
+ await execAsync(`git worktree remove "${worktreePath}" --force`, {
39
+ cwd: projectRoot,
40
+ });
41
+ }
42
+ /**
43
+ * List all worktrees
44
+ */
45
+ export function listWorktrees(projectRoot) {
46
+ const output = execSync("git worktree list --porcelain", {
47
+ cwd: projectRoot,
48
+ encoding: "utf-8",
49
+ });
50
+ const worktrees = [];
51
+ const entries = output.split("\n\n").filter(Boolean);
52
+ for (const entry of entries) {
53
+ const lines = entry.split("\n");
54
+ const pathLine = lines.find((l) => l.startsWith("worktree "));
55
+ const commitLine = lines.find((l) => l.startsWith("HEAD "));
56
+ const branchLine = lines.find((l) => l.startsWith("branch "));
57
+ if (pathLine) {
58
+ worktrees.push({
59
+ path: pathLine.replace("worktree ", ""),
60
+ commit: commitLine?.replace("HEAD ", "") || "",
61
+ branch: branchLine?.replace("branch refs/heads/", "") || "",
62
+ });
63
+ }
64
+ }
65
+ return worktrees;
66
+ }
67
+ /**
68
+ * Check if a branch exists
69
+ */
70
+ async function checkBranchExists(projectRoot, branch) {
71
+ try {
72
+ await execAsync(`git rev-parse --verify "${branch}"`, { cwd: projectRoot });
73
+ return true;
74
+ }
75
+ catch {
76
+ return false;
77
+ }
78
+ }
79
+ /**
80
+ * Merge a branch into main
81
+ */
82
+ export async function mergeBranch(projectRoot, branch, description, onConflict = "agent") {
83
+ // Checkout main and pull
84
+ await execAsync("git checkout main && git pull", { cwd: projectRoot });
85
+ // Try to merge
86
+ let mergeStrategy = "";
87
+ if (onConflict === "auto_theirs") {
88
+ mergeStrategy = "-X theirs";
89
+ }
90
+ else if (onConflict === "auto_ours") {
91
+ mergeStrategy = "-X ours";
92
+ }
93
+ try {
94
+ const { stdout } = await execAsync(`git merge --no-ff ${mergeStrategy} "${branch}" -m "merge: ${branch} - ${description}"`, { cwd: projectRoot });
95
+ // Get commit hash
96
+ const { stdout: hash } = await execAsync("git rev-parse HEAD", {
97
+ cwd: projectRoot,
98
+ });
99
+ return {
100
+ success: true,
101
+ commitHash: hash.trim(),
102
+ hasConflicts: false,
103
+ };
104
+ }
105
+ catch (error) {
106
+ // Check for conflicts
107
+ const { stdout: status } = await execAsync("git status --porcelain", {
108
+ cwd: projectRoot,
109
+ });
110
+ const conflictFiles = status
111
+ .split("\n")
112
+ .filter((line) => line.startsWith("UU ") || line.startsWith("AA "))
113
+ .map((line) => line.slice(3));
114
+ if (conflictFiles.length > 0) {
115
+ return {
116
+ success: false,
117
+ hasConflicts: true,
118
+ conflictFiles,
119
+ };
120
+ }
121
+ throw error;
122
+ }
123
+ }
124
+ /**
125
+ * Abort a merge in progress
126
+ */
127
+ export async function abortMerge(projectRoot) {
128
+ await execAsync("git merge --abort", { cwd: projectRoot });
129
+ }
130
+ /**
131
+ * Get list of conflict files
132
+ */
133
+ export async function getConflictFiles(projectRoot) {
134
+ const { stdout } = await execAsync("git diff --name-only --diff-filter=U", { cwd: projectRoot });
135
+ return stdout.split("\n").filter(Boolean);
136
+ }
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "ralph-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for autonomous PRD execution with Claude Code. Git worktree isolation, progress tracking, auto-merge.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "ralph-mcp": "dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsx watch src/index.ts",
13
+ "start": "node dist/index.js",
14
+ "prepublishOnly": "npm run build"
15
+ },
16
+ "keywords": [
17
+ "mcp",
18
+ "claude",
19
+ "claude-code",
20
+ "prd",
21
+ "ai-agent",
22
+ "autonomous",
23
+ "git-worktree"
24
+ ],
25
+ "author": "G0d2i11a",
26
+ "license": "MIT",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/G0d2i11a/ralph-mcp.git"
30
+ },
31
+ "bugs": {
32
+ "url": "https://github.com/G0d2i11a/ralph-mcp/issues"
33
+ },
34
+ "homepage": "https://github.com/G0d2i11a/ralph-mcp#readme",
35
+ "files": [
36
+ "dist",
37
+ "README.md"
38
+ ],
39
+ "engines": {
40
+ "node": ">=18"
41
+ },
42
+ "dependencies": {
43
+ "@modelcontextprotocol/sdk": "^1.25.2",
44
+ "gray-matter": "^4.0.3",
45
+ "node-notifier": "^10.0.1",
46
+ "zod": "^3.24.1"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^22.10.5",
50
+ "@types/node-notifier": "^8.0.5",
51
+ "tsx": "^4.19.2",
52
+ "typescript": "^5.7.2"
53
+ }
54
+ }