ralph-hero-mcp-server 2.4.30 → 2.4.32

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,113 @@
1
+ /**
2
+ * Transition comment format specification, builder, and parsers.
3
+ *
4
+ * Defines the canonical machine-parseable transition comment format
5
+ * (`<!-- ralph-transition: {...} -->`) and provides parsers for both
6
+ * the HTML comment format and #19's handoff_ticket markdown audit
7
+ * comment format.
8
+ *
9
+ * Pure utility module — no API dependencies.
10
+ */
11
+ // ---------------------------------------------------------------------------
12
+ // Constants
13
+ // ---------------------------------------------------------------------------
14
+ /** Regex pattern for HTML transition comments: <!-- ralph-transition: {...} --> */
15
+ export const TRANSITION_COMMENT_PATTERN = /<!-- ralph-transition: (\{.*?\}) -->/g;
16
+ /** Regex pattern for #19 handoff_ticket audit comments */
17
+ export const AUDIT_COMMENT_PATTERN = /\*\*State transition\*\*: (.+?) → (.+?) \(intent: .+?\)\n\*\*Command\*\*: ralph_(\w+)/g;
18
+ // ---------------------------------------------------------------------------
19
+ // Builder
20
+ // ---------------------------------------------------------------------------
21
+ /**
22
+ * Build a machine-parseable HTML comment encoding a transition record.
23
+ * The HTML comment is invisible in rendered GitHub markdown.
24
+ * JSON is compact (no pretty-printing) for single-line format.
25
+ */
26
+ export function buildTransitionComment(record) {
27
+ const json = JSON.stringify({
28
+ from: record.from,
29
+ to: record.to,
30
+ command: record.command,
31
+ at: record.at,
32
+ });
33
+ return `<!-- ralph-transition: ${json} -->`;
34
+ }
35
+ // ---------------------------------------------------------------------------
36
+ // Parsers
37
+ // ---------------------------------------------------------------------------
38
+ /**
39
+ * Parse transition records from HTML comment format.
40
+ * Scans text for all `<!-- ralph-transition: {...} -->` patterns.
41
+ * Gracefully handles malformed JSON (skips unparseable entries).
42
+ */
43
+ export function parseTransitionComments(text) {
44
+ const records = [];
45
+ const regex = new RegExp(TRANSITION_COMMENT_PATTERN.source, "g");
46
+ let match;
47
+ while ((match = regex.exec(text)) !== null) {
48
+ try {
49
+ const parsed = JSON.parse(match[1]);
50
+ if (parsed.from && parsed.to && parsed.command && parsed.at) {
51
+ records.push({
52
+ from: parsed.from,
53
+ to: parsed.to,
54
+ command: parsed.command,
55
+ at: parsed.at,
56
+ });
57
+ }
58
+ }
59
+ catch {
60
+ // Skip malformed JSON entries
61
+ }
62
+ }
63
+ return records;
64
+ }
65
+ /**
66
+ * Parse transition records from #19's handoff_ticket markdown audit format.
67
+ * Pattern: `**State transition**: X → Y (intent: Z)\n**Command**: ralph_cmd`
68
+ * Uses `commentCreatedAt` as the `at` timestamp since audit comments don't
69
+ * embed their own timestamp.
70
+ */
71
+ export function parseAuditComments(text, commentCreatedAt) {
72
+ const records = [];
73
+ const regex = new RegExp(AUDIT_COMMENT_PATTERN.source, "g");
74
+ let match;
75
+ while ((match = regex.exec(text)) !== null) {
76
+ records.push({
77
+ from: match[1],
78
+ to: match[2],
79
+ command: `ralph_${match[3]}`,
80
+ at: commentCreatedAt,
81
+ });
82
+ }
83
+ return records;
84
+ }
85
+ /**
86
+ * Parse transition records from a comment body, trying both formats.
87
+ * Prefers HTML comment format; falls back to audit format.
88
+ * Deduplicates records that appear in both formats (by from+to+command).
89
+ *
90
+ * This is the primary entry point for future analytics tools.
91
+ */
92
+ export function parseAllTransitions(commentBody, commentCreatedAt) {
93
+ const htmlRecords = parseTransitionComments(commentBody);
94
+ const auditRecords = parseAuditComments(commentBody, commentCreatedAt);
95
+ if (htmlRecords.length === 0) {
96
+ return auditRecords;
97
+ }
98
+ if (auditRecords.length === 0) {
99
+ return htmlRecords;
100
+ }
101
+ // Both present — deduplicate by from+to+command
102
+ const seen = new Set(htmlRecords.map((r) => `${r.from}|${r.to}|${r.command}`));
103
+ const unique = [...htmlRecords];
104
+ for (const r of auditRecords) {
105
+ const key = `${r.from}|${r.to}|${r.command}`;
106
+ if (!seen.has(key)) {
107
+ seen.add(key);
108
+ unique.push(r);
109
+ }
110
+ }
111
+ return unique;
112
+ }
113
+ //# sourceMappingURL=transition-comments.js.map
@@ -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.30",
3
+ "version": "2.4.32",
4
4
  "description": "MCP server for GitHub Projects V2 - Ralph workflow automation",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",