ralph-hero-mcp-server 2.4.34 → 2.4.36
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/dist/index.js +7 -0
- package/dist/lib/cache.js +58 -31
- package/dist/lib/helpers.js +4 -4
- package/dist/tools/dashboard-tools.js +50 -19
- package/dist/tools/project-tools.js +5 -5
- package/dist/tools/relationship-tools.js +48 -28
- package/dist/tools/view-tools.js +2 -2
- package/dist/types.js +11 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -63,6 +63,12 @@ function initGitHubClient() {
|
|
|
63
63
|
const projectNumber = resolveEnv("RALPH_GH_PROJECT_NUMBER")
|
|
64
64
|
? parseInt(resolveEnv("RALPH_GH_PROJECT_NUMBER"), 10)
|
|
65
65
|
: undefined;
|
|
66
|
+
const projectNumbers = resolveEnv("RALPH_GH_PROJECT_NUMBERS")
|
|
67
|
+
? resolveEnv("RALPH_GH_PROJECT_NUMBERS")
|
|
68
|
+
.split(",")
|
|
69
|
+
.map((s) => parseInt(s.trim(), 10))
|
|
70
|
+
.filter((n) => !isNaN(n))
|
|
71
|
+
: undefined;
|
|
66
72
|
if (!owner) {
|
|
67
73
|
console.error("[ralph-hero] Warning: RALPH_GH_OWNER not set.\n" +
|
|
68
74
|
"Most tools require this. Set in your environment or .claude/ralph-hero.local.md");
|
|
@@ -85,6 +91,7 @@ function initGitHubClient() {
|
|
|
85
91
|
owner: owner || undefined,
|
|
86
92
|
repo: repo || undefined,
|
|
87
93
|
projectNumber,
|
|
94
|
+
projectNumbers,
|
|
88
95
|
projectOwner: projectOwner || undefined,
|
|
89
96
|
});
|
|
90
97
|
}
|
package/dist/lib/cache.js
CHANGED
|
@@ -72,84 +72,111 @@ export class SessionCache {
|
|
|
72
72
|
return `query:${normalized}:${varsKey}`;
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
|
-
// ---------------------------------------------------------------------------
|
|
76
|
-
// Field Option Cache
|
|
77
|
-
// ---------------------------------------------------------------------------
|
|
78
75
|
/**
|
|
79
76
|
* Maps field names to option names to option IDs for Projects V2
|
|
80
77
|
* single-select fields. Populated by get_project, consumed by tools
|
|
81
78
|
* that need to resolve human-readable names to GraphQL node IDs.
|
|
79
|
+
*
|
|
80
|
+
* Supports multiple projects keyed by project number. When projectNumber
|
|
81
|
+
* is omitted from method calls, the first populated project is used
|
|
82
|
+
* (backward compatible with single-project callers).
|
|
82
83
|
*/
|
|
83
84
|
export class FieldOptionCache {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
fieldIds = new Map();
|
|
88
|
-
/** projectId for the cached fields */
|
|
89
|
-
projectId;
|
|
85
|
+
projects = new Map();
|
|
86
|
+
/** Track the first populated project number for backward compat */
|
|
87
|
+
defaultProjectNumber;
|
|
90
88
|
/**
|
|
91
89
|
* Populate the cache from project field data.
|
|
92
90
|
*/
|
|
93
|
-
populate(projectId, fields) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
this.fieldIds.clear();
|
|
91
|
+
populate(projectNumber, projectId, fields) {
|
|
92
|
+
const fieldMap = new Map();
|
|
93
|
+
const fieldIdMap = new Map();
|
|
97
94
|
for (const field of fields) {
|
|
98
|
-
|
|
95
|
+
fieldIdMap.set(field.name, field.id);
|
|
99
96
|
if (field.options) {
|
|
100
97
|
const optionMap = new Map();
|
|
101
98
|
for (const option of field.options) {
|
|
102
99
|
optionMap.set(option.name, option.id);
|
|
103
100
|
}
|
|
104
|
-
|
|
101
|
+
fieldMap.set(field.name, optionMap);
|
|
105
102
|
}
|
|
106
103
|
}
|
|
104
|
+
this.projects.set(projectNumber, {
|
|
105
|
+
projectId,
|
|
106
|
+
fields: fieldMap,
|
|
107
|
+
fieldIds: fieldIdMap,
|
|
108
|
+
});
|
|
109
|
+
if (this.defaultProjectNumber === undefined) {
|
|
110
|
+
this.defaultProjectNumber = projectNumber;
|
|
111
|
+
}
|
|
107
112
|
}
|
|
108
113
|
/**
|
|
109
114
|
* Resolve an option name to its ID for a given field.
|
|
110
115
|
* Returns undefined if field or option not found.
|
|
111
116
|
*/
|
|
112
|
-
resolveOptionId(fieldName, optionName) {
|
|
113
|
-
|
|
117
|
+
resolveOptionId(fieldName, optionName, projectNumber) {
|
|
118
|
+
const entry = this.resolveEntry(projectNumber);
|
|
119
|
+
return entry?.fields.get(fieldName)?.get(optionName);
|
|
114
120
|
}
|
|
115
121
|
/**
|
|
116
122
|
* Get the field ID for a field name.
|
|
117
123
|
*/
|
|
118
|
-
getFieldId(fieldName) {
|
|
119
|
-
|
|
124
|
+
getFieldId(fieldName, projectNumber) {
|
|
125
|
+
const entry = this.resolveEntry(projectNumber);
|
|
126
|
+
return entry?.fieldIds.get(fieldName);
|
|
120
127
|
}
|
|
121
128
|
/**
|
|
122
129
|
* Get the project ID for the cached fields.
|
|
123
130
|
*/
|
|
124
|
-
getProjectId() {
|
|
125
|
-
|
|
131
|
+
getProjectId(projectNumber) {
|
|
132
|
+
const entry = this.resolveEntry(projectNumber);
|
|
133
|
+
return entry?.projectId;
|
|
126
134
|
}
|
|
127
135
|
/**
|
|
128
136
|
* Check if the cache has been populated.
|
|
137
|
+
* When projectNumber is provided, checks that specific project.
|
|
138
|
+
* When omitted, checks if any project is populated.
|
|
129
139
|
*/
|
|
130
|
-
isPopulated() {
|
|
131
|
-
|
|
140
|
+
isPopulated(projectNumber) {
|
|
141
|
+
if (projectNumber !== undefined) {
|
|
142
|
+
return this.projects.has(projectNumber);
|
|
143
|
+
}
|
|
144
|
+
return this.projects.size > 0;
|
|
132
145
|
}
|
|
133
146
|
/**
|
|
134
147
|
* Get all option names for a field.
|
|
135
148
|
*/
|
|
136
|
-
getOptionNames(fieldName) {
|
|
137
|
-
const
|
|
149
|
+
getOptionNames(fieldName, projectNumber) {
|
|
150
|
+
const entry = this.resolveEntry(projectNumber);
|
|
151
|
+
const optionMap = entry?.fields.get(fieldName);
|
|
138
152
|
return optionMap ? Array.from(optionMap.keys()) : [];
|
|
139
153
|
}
|
|
140
154
|
/**
|
|
141
155
|
* Get all field names.
|
|
142
156
|
*/
|
|
143
|
-
getFieldNames() {
|
|
144
|
-
|
|
157
|
+
getFieldNames(projectNumber) {
|
|
158
|
+
const entry = this.resolveEntry(projectNumber);
|
|
159
|
+
return entry ? Array.from(entry.fieldIds.keys()) : [];
|
|
145
160
|
}
|
|
146
161
|
/**
|
|
147
|
-
* Clear the cache.
|
|
162
|
+
* Clear the cache (all projects).
|
|
148
163
|
*/
|
|
149
164
|
clear() {
|
|
150
|
-
this.
|
|
151
|
-
this.
|
|
152
|
-
|
|
165
|
+
this.projects.clear();
|
|
166
|
+
this.defaultProjectNumber = undefined;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Resolve the cache entry for a project number.
|
|
170
|
+
* Falls back to the first populated project when projectNumber is omitted.
|
|
171
|
+
*/
|
|
172
|
+
resolveEntry(projectNumber) {
|
|
173
|
+
if (projectNumber !== undefined) {
|
|
174
|
+
return this.projects.get(projectNumber);
|
|
175
|
+
}
|
|
176
|
+
if (this.defaultProjectNumber !== undefined) {
|
|
177
|
+
return this.projects.get(this.defaultProjectNumber);
|
|
178
|
+
}
|
|
179
|
+
return undefined;
|
|
153
180
|
}
|
|
154
181
|
}
|
|
155
182
|
//# sourceMappingURL=cache.js.map
|
package/dist/lib/helpers.js
CHANGED
|
@@ -50,14 +50,14 @@ async function fetchProjectForCache(client, owner, number) {
|
|
|
50
50
|
// Helper: Ensure field option cache is populated
|
|
51
51
|
// ---------------------------------------------------------------------------
|
|
52
52
|
export async function ensureFieldCache(client, fieldCache, owner, projectNumber) {
|
|
53
|
-
if (fieldCache.isPopulated())
|
|
53
|
+
if (fieldCache.isPopulated(projectNumber))
|
|
54
54
|
return;
|
|
55
55
|
// Fetch project to populate cache - try user first, then org
|
|
56
56
|
const project = await fetchProjectForCache(client, owner, projectNumber);
|
|
57
57
|
if (!project) {
|
|
58
58
|
throw new Error(`Project #${projectNumber} not found for owner "${owner}"`);
|
|
59
59
|
}
|
|
60
|
-
fieldCache.populate(project.id, project.fields.nodes.map((f) => ({
|
|
60
|
+
fieldCache.populate(projectNumber, project.id, project.fields.nodes.map((f) => ({
|
|
61
61
|
id: f.id,
|
|
62
62
|
name: f.name,
|
|
63
63
|
options: f.options,
|
|
@@ -269,9 +269,9 @@ export function resolveConfig(client, args) {
|
|
|
269
269
|
// ---------------------------------------------------------------------------
|
|
270
270
|
export function resolveFullConfig(client, args) {
|
|
271
271
|
const { owner, repo } = resolveConfig(client, args);
|
|
272
|
-
const projectNumber = client.config.projectNumber;
|
|
272
|
+
const projectNumber = args.projectNumber ?? client.config.projectNumber;
|
|
273
273
|
if (!projectNumber) {
|
|
274
|
-
throw new Error("projectNumber is required (set RALPH_GH_PROJECT_NUMBER env var)");
|
|
274
|
+
throw new Error("projectNumber is required (set RALPH_GH_PROJECT_NUMBER env var or pass explicitly)");
|
|
275
275
|
}
|
|
276
276
|
const projectOwner = resolveProjectOwner(client.config);
|
|
277
277
|
if (!projectOwner) {
|
|
@@ -8,19 +8,19 @@
|
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
import { paginateConnection } from "../lib/pagination.js";
|
|
10
10
|
import { buildDashboard, formatMarkdown, formatAscii, DEFAULT_HEALTH_CONFIG, } from "../lib/dashboard.js";
|
|
11
|
-
import { toolSuccess, toolError, resolveProjectOwner } from "../types.js";
|
|
11
|
+
import { toolSuccess, toolError, resolveProjectOwner, resolveProjectNumbers } from "../types.js";
|
|
12
12
|
import { calculateMetrics, DEFAULT_METRICS_CONFIG, } from "../lib/metrics.js";
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
14
14
|
// Helper: Ensure field option cache is populated
|
|
15
15
|
// ---------------------------------------------------------------------------
|
|
16
16
|
async function ensureFieldCache(client, fieldCache, owner, projectNumber) {
|
|
17
|
-
if (fieldCache.isPopulated())
|
|
17
|
+
if (fieldCache.isPopulated(projectNumber))
|
|
18
18
|
return;
|
|
19
19
|
const project = await fetchProjectForCache(client, owner, projectNumber);
|
|
20
20
|
if (!project) {
|
|
21
21
|
throw new Error(`Project #${projectNumber} not found for owner "${owner}"`);
|
|
22
22
|
}
|
|
23
|
-
fieldCache.populate(project.id, project.fields.nodes.map((f) => ({
|
|
23
|
+
fieldCache.populate(projectNumber, project.id, project.fields.nodes.map((f) => ({
|
|
24
24
|
id: f.id,
|
|
25
25
|
name: f.name,
|
|
26
26
|
options: f.options,
|
|
@@ -69,8 +69,10 @@ function getFieldValue(item, fieldName) {
|
|
|
69
69
|
}
|
|
70
70
|
/**
|
|
71
71
|
* Convert raw GraphQL project items to DashboardItem[].
|
|
72
|
+
* When projectNumber/projectTitle are provided, they are set on each item
|
|
73
|
+
* for multi-project dashboard support.
|
|
72
74
|
*/
|
|
73
|
-
export function toDashboardItems(raw) {
|
|
75
|
+
export function toDashboardItems(raw, projectNumber, projectTitle) {
|
|
74
76
|
const items = [];
|
|
75
77
|
for (const r of raw) {
|
|
76
78
|
// Only include issues (not PRs or drafts)
|
|
@@ -88,6 +90,8 @@ export function toDashboardItems(raw) {
|
|
|
88
90
|
estimate: getFieldValue(r, "Estimate"),
|
|
89
91
|
assignees: r.content.assignees?.nodes?.map((a) => a.login) ?? [],
|
|
90
92
|
blockedBy: [], // blockedBy requires separate queries; omit for now
|
|
93
|
+
...(projectNumber !== undefined ? { projectNumber } : {}),
|
|
94
|
+
...(projectTitle !== undefined ? { projectTitle } : {}),
|
|
91
95
|
});
|
|
92
96
|
}
|
|
93
97
|
return items;
|
|
@@ -148,6 +152,10 @@ export function registerDashboardTools(server, client, fieldCache) {
|
|
|
148
152
|
.string()
|
|
149
153
|
.optional()
|
|
150
154
|
.describe("GitHub owner. Defaults to GITHUB_OWNER env var"),
|
|
155
|
+
projectNumbers: z
|
|
156
|
+
.array(z.coerce.number())
|
|
157
|
+
.optional()
|
|
158
|
+
.describe("Project numbers to include. Defaults to RALPH_GH_PROJECT_NUMBERS or single configured project."),
|
|
151
159
|
format: z
|
|
152
160
|
.enum(["json", "markdown", "ascii"])
|
|
153
161
|
.optional()
|
|
@@ -205,23 +213,45 @@ export function registerDashboardTools(server, client, fieldCache) {
|
|
|
205
213
|
}, async (args) => {
|
|
206
214
|
try {
|
|
207
215
|
const owner = args.owner || resolveProjectOwner(client.config);
|
|
208
|
-
const projectNumber = client.config.projectNumber;
|
|
209
216
|
if (!owner) {
|
|
210
217
|
return toolError("owner is required");
|
|
211
218
|
}
|
|
212
|
-
|
|
213
|
-
|
|
219
|
+
// Resolve project numbers
|
|
220
|
+
const projectNumbers = args.projectNumbers
|
|
221
|
+
?? resolveProjectNumbers(client.config);
|
|
222
|
+
if (projectNumbers.length === 0) {
|
|
223
|
+
return toolError("No project numbers configured. Set RALPH_GH_PROJECT_NUMBER or RALPH_GH_PROJECT_NUMBERS.");
|
|
214
224
|
}
|
|
215
|
-
//
|
|
216
|
-
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
225
|
+
// Fetch items from all projects
|
|
226
|
+
const allItems = [];
|
|
227
|
+
const fetchWarnings = [];
|
|
228
|
+
for (const pn of projectNumbers) {
|
|
229
|
+
try {
|
|
230
|
+
await ensureFieldCache(client, fieldCache, owner, pn);
|
|
231
|
+
}
|
|
232
|
+
catch (e) {
|
|
233
|
+
fetchWarnings.push(`Project #${pn}: ${e instanceof Error ? e.message : String(e)}, skipping`);
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
const projectId = fieldCache.getProjectId(pn);
|
|
237
|
+
if (!projectId) {
|
|
238
|
+
fetchWarnings.push(`Project #${pn}: could not resolve project ID, skipping`);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
// Fetch project title
|
|
242
|
+
let projectTitle;
|
|
243
|
+
try {
|
|
244
|
+
const titleResult = await client.projectQuery(`query($projectId: ID!) { node(id: $projectId) { ... on ProjectV2 { title } } }`, { projectId });
|
|
245
|
+
projectTitle = titleResult.node?.title;
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
// Non-fatal -- proceed without title
|
|
249
|
+
}
|
|
250
|
+
// Fetch items
|
|
251
|
+
const result = await paginateConnection((q, v) => client.projectQuery(q, v), DASHBOARD_ITEMS_QUERY, { projectId, first: 100 }, "node.items", { maxItems: 500 });
|
|
252
|
+
const items = toDashboardItems(result.nodes, pn, projectTitle);
|
|
253
|
+
allItems.push(...items);
|
|
220
254
|
}
|
|
221
|
-
// Fetch all project items
|
|
222
|
-
const result = await paginateConnection((q, v) => client.projectQuery(q, v), DASHBOARD_ITEMS_QUERY, { projectId, first: 100 }, "node.items", { maxItems: 500 });
|
|
223
|
-
// Convert to dashboard items
|
|
224
|
-
const dashboardItems = toDashboardItems(result.nodes);
|
|
225
255
|
// Build health config
|
|
226
256
|
const healthConfig = {
|
|
227
257
|
...DEFAULT_HEALTH_CONFIG,
|
|
@@ -231,8 +261,8 @@ export function registerDashboardTools(server, client, fieldCache) {
|
|
|
231
261
|
doneWindowDays: args.doneWindowDays ?? 7,
|
|
232
262
|
archiveThresholdDays: args.archiveThresholdDays ?? 14,
|
|
233
263
|
};
|
|
234
|
-
// Build dashboard
|
|
235
|
-
const dashboard = buildDashboard(
|
|
264
|
+
// Build dashboard from merged items
|
|
265
|
+
const dashboard = buildDashboard(allItems, healthConfig);
|
|
236
266
|
// Strip health if not requested
|
|
237
267
|
if (!args.includeHealth) {
|
|
238
268
|
dashboard.health = { ok: true, warnings: [] };
|
|
@@ -251,7 +281,7 @@ export function registerDashboardTools(server, client, fieldCache) {
|
|
|
251
281
|
atRiskThreshold: args.atRiskThreshold ?? 2,
|
|
252
282
|
offTrackThreshold: args.offTrackThreshold ?? 6,
|
|
253
283
|
};
|
|
254
|
-
metrics = calculateMetrics(
|
|
284
|
+
metrics = calculateMetrics(allItems, dashboard, metricsConfig);
|
|
255
285
|
}
|
|
256
286
|
// Format output
|
|
257
287
|
const format = args.format ?? "json";
|
|
@@ -266,6 +296,7 @@ export function registerDashboardTools(server, client, fieldCache) {
|
|
|
266
296
|
...dashboard,
|
|
267
297
|
...(formatted !== undefined ? { formatted } : {}),
|
|
268
298
|
...(metrics !== undefined ? { metrics } : {}),
|
|
299
|
+
...(fetchWarnings.length > 0 ? { fetchWarnings } : {}),
|
|
269
300
|
});
|
|
270
301
|
}
|
|
271
302
|
catch (error) {
|
|
@@ -81,16 +81,16 @@ const ESTIMATE_OPTIONS = [
|
|
|
81
81
|
// Helper: Ensure field option cache is populated
|
|
82
82
|
// ---------------------------------------------------------------------------
|
|
83
83
|
async function ensureFieldCache(client, fieldCache, owner, projectNumber) {
|
|
84
|
-
if (fieldCache.isPopulated())
|
|
84
|
+
if (fieldCache.isPopulated(projectNumber))
|
|
85
85
|
return;
|
|
86
86
|
const project = await fetchProject(client, owner, projectNumber);
|
|
87
87
|
if (!project) {
|
|
88
88
|
throw new Error(`Project #${projectNumber} not found for owner "${owner}"`);
|
|
89
89
|
}
|
|
90
|
-
populateFieldCache(fieldCache, project);
|
|
90
|
+
populateFieldCache(fieldCache, project, projectNumber);
|
|
91
91
|
}
|
|
92
|
-
function populateFieldCache(fieldCache, project) {
|
|
93
|
-
fieldCache.populate(project.id, project.fields.nodes.map((f) => ({
|
|
92
|
+
function populateFieldCache(fieldCache, project, projectNumber) {
|
|
93
|
+
fieldCache.populate(projectNumber, project.id, project.fields.nodes.map((f) => ({
|
|
94
94
|
id: f.id,
|
|
95
95
|
name: f.name,
|
|
96
96
|
options: f.options,
|
|
@@ -205,7 +205,7 @@ export function registerProjectTools(server, client, fieldCache) {
|
|
|
205
205
|
return toolError(`Project #${number} not found for owner "${owner}"`);
|
|
206
206
|
}
|
|
207
207
|
// Populate field cache
|
|
208
|
-
populateFieldCache(fieldCache, result);
|
|
208
|
+
populateFieldCache(fieldCache, result, number);
|
|
209
209
|
// Format response
|
|
210
210
|
const fields = result.fields.nodes.map((f) => ({
|
|
211
211
|
id: f.id,
|
|
@@ -325,7 +325,7 @@ export function registerRelationshipTools(server, client, fieldCache) {
|
|
|
325
325
|
// -------------------------------------------------------------------------
|
|
326
326
|
// ralph_hero__advance_children
|
|
327
327
|
// -------------------------------------------------------------------------
|
|
328
|
-
server.tool("ralph_hero__advance_children", "Advance
|
|
328
|
+
server.tool("ralph_hero__advance_children", "Advance issues to a target workflow state. Provide either 'number' (parent issue, advances sub-issues) or 'issues' (explicit list of issue numbers). Only advances issues in earlier workflow states. Returns what changed, what was skipped, and any errors.", {
|
|
329
329
|
owner: z
|
|
330
330
|
.string()
|
|
331
331
|
.optional()
|
|
@@ -334,12 +334,24 @@ export function registerRelationshipTools(server, client, fieldCache) {
|
|
|
334
334
|
.string()
|
|
335
335
|
.optional()
|
|
336
336
|
.describe("Repository name. Defaults to GITHUB_REPO env var"),
|
|
337
|
-
number: z
|
|
337
|
+
number: z
|
|
338
|
+
.coerce.number()
|
|
339
|
+
.optional()
|
|
340
|
+
.describe("Parent issue number (resolves sub-issues automatically)"),
|
|
341
|
+
issues: z
|
|
342
|
+
.array(z.coerce.number())
|
|
343
|
+
.optional()
|
|
344
|
+
.describe("Explicit list of issue numbers to advance (alternative to parent number)"),
|
|
338
345
|
targetState: z
|
|
339
346
|
.string()
|
|
340
|
-
.describe("State to advance
|
|
347
|
+
.describe("State to advance issues to (e.g., 'Research Needed', 'Ready for Plan')"),
|
|
341
348
|
}, async (args) => {
|
|
342
349
|
try {
|
|
350
|
+
// Validate: at least one of number or issues must be provided
|
|
351
|
+
if (args.number === undefined && (!args.issues || args.issues.length === 0)) {
|
|
352
|
+
return toolError("Either 'number' (parent issue) or 'issues' (explicit list) is required. " +
|
|
353
|
+
"Recovery: provide one of these parameters.");
|
|
354
|
+
}
|
|
343
355
|
// Validate target state
|
|
344
356
|
if (!isValidState(args.targetState)) {
|
|
345
357
|
return toolError(`Unknown target state '${args.targetState}'. ` +
|
|
@@ -358,24 +370,32 @@ export function registerRelationshipTools(server, client, fieldCache) {
|
|
|
358
370
|
}
|
|
359
371
|
// Ensure field cache is populated
|
|
360
372
|
await ensureFieldCache(client, fieldCache, projectOwner, projectNumber);
|
|
361
|
-
//
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
373
|
+
// Build issue list: from explicit `issues` param or from parent's sub-issues
|
|
374
|
+
let issueNumbers;
|
|
375
|
+
if (args.issues && args.issues.length > 0) {
|
|
376
|
+
// Explicit issue list takes precedence
|
|
377
|
+
issueNumbers = args.issues;
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
// Fetch sub-issues from parent
|
|
381
|
+
const result = await client.query(`query($owner: String!, $repo: String!, $number: Int!) {
|
|
382
|
+
repository(owner: $owner, name: $repo) {
|
|
383
|
+
issue(number: $number) {
|
|
384
|
+
number
|
|
385
|
+
title
|
|
386
|
+
subIssues(first: 50) {
|
|
387
|
+
nodes { id number title state }
|
|
388
|
+
}
|
|
369
389
|
}
|
|
370
390
|
}
|
|
391
|
+
}`, { owner, repo, number: args.number });
|
|
392
|
+
const parentIssue = result.repository?.issue;
|
|
393
|
+
if (!parentIssue) {
|
|
394
|
+
return toolError(`Issue #${args.number} not found in ${owner}/${repo}`);
|
|
395
|
+
}
|
|
396
|
+
issueNumbers = parentIssue.subIssues.nodes.map((si) => si.number);
|
|
371
397
|
}
|
|
372
|
-
|
|
373
|
-
const parentIssue = result.repository?.issue;
|
|
374
|
-
if (!parentIssue) {
|
|
375
|
-
return toolError(`Issue #${args.number} not found in ${owner}/${repo}`);
|
|
376
|
-
}
|
|
377
|
-
const subIssues = parentIssue.subIssues.nodes;
|
|
378
|
-
if (subIssues.length === 0) {
|
|
398
|
+
if (issueNumbers.length === 0) {
|
|
379
399
|
return toolSuccess({
|
|
380
400
|
advanced: [],
|
|
381
401
|
skipped: [],
|
|
@@ -385,22 +405,22 @@ export function registerRelationshipTools(server, client, fieldCache) {
|
|
|
385
405
|
const advanced = [];
|
|
386
406
|
const skipped = [];
|
|
387
407
|
const errors = [];
|
|
388
|
-
for (const
|
|
408
|
+
for (const issueNum of issueNumbers) {
|
|
389
409
|
try {
|
|
390
410
|
// Get current workflow state
|
|
391
|
-
const currentState = await getCurrentFieldValue(client, fieldCache, owner, repo,
|
|
411
|
+
const currentState = await getCurrentFieldValue(client, fieldCache, owner, repo, issueNum, "Workflow State");
|
|
392
412
|
if (!currentState) {
|
|
393
413
|
skipped.push({
|
|
394
|
-
number:
|
|
414
|
+
number: issueNum,
|
|
395
415
|
currentState: "unknown",
|
|
396
416
|
reason: "No workflow state set on issue",
|
|
397
417
|
});
|
|
398
418
|
continue;
|
|
399
419
|
}
|
|
400
|
-
// Only advance if
|
|
420
|
+
// Only advance if issue is in an earlier state
|
|
401
421
|
if (!isEarlierState(currentState, args.targetState)) {
|
|
402
422
|
skipped.push({
|
|
403
|
-
number:
|
|
423
|
+
number: issueNum,
|
|
404
424
|
currentState,
|
|
405
425
|
reason: currentState === args.targetState
|
|
406
426
|
? "Already at target state"
|
|
@@ -408,13 +428,13 @@ export function registerRelationshipTools(server, client, fieldCache) {
|
|
|
408
428
|
});
|
|
409
429
|
continue;
|
|
410
430
|
}
|
|
411
|
-
// Advance the
|
|
412
|
-
const projectItemId = await resolveProjectItemId(client, fieldCache, owner, repo,
|
|
431
|
+
// Advance the issue
|
|
432
|
+
const projectItemId = await resolveProjectItemId(client, fieldCache, owner, repo, issueNum);
|
|
413
433
|
await updateProjectItemField(client, fieldCache, projectItemId, "Workflow State", args.targetState);
|
|
414
434
|
// Sync default Status field (best-effort, one-way)
|
|
415
435
|
await syncStatusField(client, fieldCache, projectItemId, args.targetState);
|
|
416
436
|
advanced.push({
|
|
417
|
-
number:
|
|
437
|
+
number: issueNum,
|
|
418
438
|
fromState: currentState,
|
|
419
439
|
toState: args.targetState,
|
|
420
440
|
});
|
|
@@ -422,8 +442,8 @@ export function registerRelationshipTools(server, client, fieldCache) {
|
|
|
422
442
|
catch (error) {
|
|
423
443
|
const message = error instanceof Error ? error.message : String(error);
|
|
424
444
|
errors.push({
|
|
425
|
-
number:
|
|
426
|
-
error: `Failed to update: ${message}. Recovery: retry advance_children or update this
|
|
445
|
+
number: issueNum,
|
|
446
|
+
error: `Failed to update: ${message}. Recovery: retry advance_children or update this issue manually.`,
|
|
427
447
|
});
|
|
428
448
|
}
|
|
429
449
|
}
|
package/dist/tools/view-tools.js
CHANGED
|
@@ -143,7 +143,7 @@ export function registerViewTools(server, client, fieldCache) {
|
|
|
143
143
|
});
|
|
144
144
|
}
|
|
145
145
|
async function ensureFieldCache(client, fieldCache, owner, projectNumber) {
|
|
146
|
-
if (fieldCache.isPopulated())
|
|
146
|
+
if (fieldCache.isPopulated(projectNumber))
|
|
147
147
|
return;
|
|
148
148
|
const QUERY = `
|
|
149
149
|
query($owner: String!, $number: Int!) {
|
|
@@ -171,7 +171,7 @@ async function ensureFieldCache(client, fieldCache, owner, projectNumber) {
|
|
|
171
171
|
const result = await client.projectQuery(QUERY.replace("OWNER_TYPE", ownerType), { owner, number: projectNumber }, { cache: true });
|
|
172
172
|
const project = result[ownerType]?.projectV2;
|
|
173
173
|
if (project) {
|
|
174
|
-
fieldCache.populate(project.id, project.fields.nodes.map((f) => ({
|
|
174
|
+
fieldCache.populate(projectNumber, project.id, project.fields.nodes.map((f) => ({
|
|
175
175
|
id: f.id,
|
|
176
176
|
name: f.name,
|
|
177
177
|
options: f.options,
|
package/dist/types.js
CHANGED
|
@@ -18,4 +18,15 @@ export function toolError(message) {
|
|
|
18
18
|
export function resolveProjectOwner(config) {
|
|
19
19
|
return config.projectOwner || config.owner;
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Return all configured project numbers.
|
|
23
|
+
* Prefers projectNumbers array; falls back to single projectNumber.
|
|
24
|
+
*/
|
|
25
|
+
export function resolveProjectNumbers(config) {
|
|
26
|
+
if (config.projectNumbers?.length)
|
|
27
|
+
return config.projectNumbers;
|
|
28
|
+
if (config.projectNumber)
|
|
29
|
+
return [config.projectNumber];
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
21
32
|
//# sourceMappingURL=types.js.map
|