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.
- package/LICENSE +21 -0
- package/README.md +131 -0
- package/dist/db/client.d.ts +7 -0
- package/dist/db/client.js +55 -0
- package/dist/db/schema.d.ts +540 -0
- package/dist/db/schema.js +57 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +278 -0
- package/dist/store/state.d.ts +55 -0
- package/dist/store/state.js +205 -0
- package/dist/tools/get.d.ts +40 -0
- package/dist/tools/get.js +48 -0
- package/dist/tools/merge.d.ts +50 -0
- package/dist/tools/merge.js +384 -0
- package/dist/tools/set-agent-id.d.ts +18 -0
- package/dist/tools/set-agent-id.js +24 -0
- package/dist/tools/start.d.ts +42 -0
- package/dist/tools/start.js +96 -0
- package/dist/tools/status.d.ts +35 -0
- package/dist/tools/status.js +53 -0
- package/dist/tools/stop.d.ts +24 -0
- package/dist/tools/stop.js +52 -0
- package/dist/tools/update.d.ts +28 -0
- package/dist/tools/update.js +71 -0
- package/dist/utils/agent.d.ts +22 -0
- package/dist/utils/agent.js +110 -0
- package/dist/utils/merge-helpers.d.ts +48 -0
- package/dist/utils/merge-helpers.js +213 -0
- package/dist/utils/prd-parser.d.ts +22 -0
- package/dist/utils/prd-parser.js +137 -0
- package/dist/utils/worktree.d.ts +34 -0
- package/dist/utils/worktree.js +136 -0
- package/package.json +54 -0
|
@@ -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
|
+
}
|