ralph-hero-mcp-server 2.4.28 → 2.4.31

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.
@@ -20,6 +20,7 @@ const SEMANTIC_INTENTS = {
20
20
  ralph_plan: "Plan in Review",
21
21
  ralph_impl: "In Review",
22
22
  ralph_review: "In Progress",
23
+ ralph_merge: "Done",
23
24
  },
24
25
  __ESCALATE__: { "*": "Human Needed" },
25
26
  __CLOSE__: { "*": "Done" },
@@ -40,6 +41,7 @@ const COMMAND_ALLOWED_STATES = {
40
41
  ralph_impl: ["In Progress", "In Review", "Human Needed"],
41
42
  ralph_review: ["In Progress", "Ready for Plan", "Human Needed"],
42
43
  ralph_hero: ["In Review", "Human Needed"],
44
+ ralph_merge: ["Done", "Human Needed"],
43
45
  };
44
46
  // --- Helpers ---
45
47
  function isSemanticIntent(state) {
@@ -231,6 +231,197 @@ export function registerProjectTools(server, client, fieldCache) {
231
231
  }
232
232
  });
233
233
  // -------------------------------------------------------------------------
234
+ // ralph_hero__list_projects
235
+ // -------------------------------------------------------------------------
236
+ server.tool("ralph_hero__list_projects", "List all GitHub Projects V2 for an owner (user or organization). Returns project summaries with item/field/view counts. Supports open/closed filtering.", {
237
+ owner: z
238
+ .string()
239
+ .optional()
240
+ .describe("GitHub owner (user or org). Defaults to RALPH_GH_PROJECT_OWNER or RALPH_GH_OWNER env var"),
241
+ state: z
242
+ .enum(["open", "closed", "all"])
243
+ .optional()
244
+ .default("open")
245
+ .describe('Filter by project state: "open" (default), "closed", or "all"'),
246
+ limit: z
247
+ .number()
248
+ .optional()
249
+ .default(50)
250
+ .describe("Maximum number of projects to return (default: 50, max: 100)"),
251
+ }, async (args) => {
252
+ try {
253
+ const owner = args.owner || resolveProjectOwner(client.config);
254
+ if (!owner) {
255
+ return toolError("owner is required (set RALPH_GH_PROJECT_OWNER or RALPH_GH_OWNER env var or pass explicitly)");
256
+ }
257
+ const maxItems = Math.min(args.limit ?? 50, 100);
258
+ const LIST_PROJECTS_QUERY = `
259
+ query($owner: String!, $cursor: String, $first: Int!) {
260
+ OWNER_TYPE(login: $owner) {
261
+ projectsV2(first: $first, after: $cursor, orderBy: {field: TITLE, direction: ASC}) {
262
+ totalCount
263
+ pageInfo { hasNextPage endCursor }
264
+ nodes {
265
+ id
266
+ number
267
+ title
268
+ shortDescription
269
+ public
270
+ closed
271
+ url
272
+ items { totalCount }
273
+ fields { totalCount }
274
+ views { totalCount }
275
+ }
276
+ }
277
+ }
278
+ }
279
+ `;
280
+ let allProjects = [];
281
+ let totalCount;
282
+ for (const ownerType of ["user", "organization"]) {
283
+ try {
284
+ const result = await paginateConnection((q, vars) => client.projectQuery(q, vars), LIST_PROJECTS_QUERY.replace("OWNER_TYPE", ownerType), { owner }, `${ownerType}.projectsV2`, { maxItems });
285
+ allProjects = result.nodes;
286
+ totalCount = result.totalCount;
287
+ break;
288
+ }
289
+ catch {
290
+ // Try next owner type
291
+ }
292
+ }
293
+ // Client-side state filtering
294
+ const filtered = args.state === "all"
295
+ ? allProjects
296
+ : args.state === "closed"
297
+ ? allProjects.filter((p) => p.closed)
298
+ : allProjects.filter((p) => !p.closed);
299
+ const projects = filtered.map((p) => ({
300
+ id: p.id,
301
+ number: p.number,
302
+ title: p.title,
303
+ shortDescription: p.shortDescription,
304
+ public: p.public,
305
+ closed: p.closed,
306
+ url: p.url,
307
+ itemCount: p.items?.totalCount ?? 0,
308
+ fieldCount: p.fields?.totalCount ?? 0,
309
+ viewCount: p.views?.totalCount ?? 0,
310
+ }));
311
+ return toolSuccess({
312
+ owner,
313
+ state: args.state,
314
+ projects,
315
+ totalCount: totalCount ?? projects.length,
316
+ returnedCount: projects.length,
317
+ });
318
+ }
319
+ catch (error) {
320
+ const message = error instanceof Error ? error.message : String(error);
321
+ return toolError(`Failed to list projects: ${message}`);
322
+ }
323
+ });
324
+ // -------------------------------------------------------------------------
325
+ // ralph_hero__copy_project
326
+ // -------------------------------------------------------------------------
327
+ server.tool("ralph_hero__copy_project", "Copy (duplicate) a GitHub Project V2, preserving views, custom fields, workflows, and insights. Does NOT copy items, collaborators, team links, or repository links.", {
328
+ sourceProjectNumber: z
329
+ .number()
330
+ .describe("Project number of the source project to copy"),
331
+ sourceOwner: z
332
+ .string()
333
+ .optional()
334
+ .describe("Owner of the source project. Defaults to RALPH_GH_PROJECT_OWNER or RALPH_GH_OWNER env var"),
335
+ title: z.string().describe("Title for the new project"),
336
+ targetOwner: z
337
+ .string()
338
+ .optional()
339
+ .describe("Owner for the new project. Defaults to sourceOwner. Supports cross-owner copy (e.g., personal to org)"),
340
+ includeDraftIssues: z
341
+ .boolean()
342
+ .optional()
343
+ .default(false)
344
+ .describe("Include draft issues from the source project in the copy (default: false)"),
345
+ }, async (args) => {
346
+ try {
347
+ const sourceOwner = args.sourceOwner || resolveProjectOwner(client.config);
348
+ if (!sourceOwner) {
349
+ return toolError("sourceOwner is required (set RALPH_GH_PROJECT_OWNER or RALPH_GH_OWNER env var or pass explicitly)");
350
+ }
351
+ // Step 1: Resolve source project node ID via fetchProject
352
+ const sourceProject = await fetchProject(client, sourceOwner, args.sourceProjectNumber);
353
+ if (!sourceProject) {
354
+ return toolError(`Source project #${args.sourceProjectNumber} not found for owner "${sourceOwner}"`);
355
+ }
356
+ // Step 2: Resolve target owner node ID (try user first, then org)
357
+ const targetOwnerLogin = args.targetOwner || sourceOwner;
358
+ let targetOwnerId;
359
+ try {
360
+ const userResult = await client.query(`query($login: String!) {
361
+ user(login: $login) { id }
362
+ }`, { login: targetOwnerLogin }, { cache: true });
363
+ targetOwnerId = userResult.user?.id;
364
+ }
365
+ catch {
366
+ // Not a user, try org below
367
+ }
368
+ if (!targetOwnerId) {
369
+ try {
370
+ const orgResult = await client.query(`query($login: String!) {
371
+ organization(login: $login) { id }
372
+ }`, { login: targetOwnerLogin }, { cache: true });
373
+ targetOwnerId = orgResult.organization?.id;
374
+ }
375
+ catch {
376
+ // Not an org either
377
+ }
378
+ }
379
+ if (!targetOwnerId) {
380
+ return toolError(`Target owner "${targetOwnerLogin}" not found as user or organization`);
381
+ }
382
+ // Step 3: Execute copyProjectV2 mutation
383
+ const copyResult = await client.projectMutate(`mutation($projectId: ID!, $ownerId: ID!, $title: String!, $includeDraftIssues: Boolean!) {
384
+ copyProjectV2(input: {
385
+ projectId: $projectId
386
+ ownerId: $ownerId
387
+ title: $title
388
+ includeDraftIssues: $includeDraftIssues
389
+ }) {
390
+ projectV2 {
391
+ id
392
+ number
393
+ url
394
+ title
395
+ }
396
+ }
397
+ }`, {
398
+ projectId: sourceProject.id,
399
+ ownerId: targetOwnerId,
400
+ title: args.title,
401
+ includeDraftIssues: args.includeDraftIssues ?? false,
402
+ });
403
+ const newProject = copyResult.copyProjectV2.projectV2;
404
+ return toolSuccess({
405
+ project: {
406
+ id: newProject.id,
407
+ number: newProject.number,
408
+ url: newProject.url,
409
+ title: newProject.title,
410
+ },
411
+ copiedFrom: {
412
+ number: args.sourceProjectNumber,
413
+ owner: sourceOwner,
414
+ title: sourceProject.title,
415
+ },
416
+ note: "Copied views, custom fields, workflows, and insights. Items, collaborators, team links, and repository links were NOT copied.",
417
+ });
418
+ }
419
+ catch (error) {
420
+ const message = error instanceof Error ? error.message : String(error);
421
+ return toolError(`Failed to copy project: ${message}`);
422
+ }
423
+ });
424
+ // -------------------------------------------------------------------------
234
425
  // ralph_hero__list_project_items
235
426
  // -------------------------------------------------------------------------
236
427
  server.tool("ralph_hero__list_project_items", "List items in a GitHub Project V2, optionally filtered by Workflow State, Estimate, or Priority", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ralph-hero-mcp-server",
3
- "version": "2.4.28",
3
+ "version": "2.4.31",
4
4
  "description": "MCP server for GitHub Projects V2 - Ralph workflow automation",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",