@iloom/cli 0.7.5 → 0.8.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 +1 -1
- package/README.md +32 -3
- package/dist/{ClaudeContextManager-Y2YJC6BU.js → ClaudeContextManager-RDP6CLK6.js} +5 -5
- package/dist/{ClaudeService-NDVFQRKC.js → ClaudeService-FKPOQRA4.js} +4 -4
- package/dist/GitHubService-ACZVNTJE.js +12 -0
- package/dist/{LoomLauncher-U2B3VHPC.js → LoomLauncher-NHZMEVTQ.js} +5 -5
- package/dist/{MetadataManager-XJ2YB762.js → MetadataManager-W3C54UYT.js} +2 -2
- package/dist/{PRManager-6ZJZRG5Z.js → PRManager-XLTVG6YG.js} +5 -5
- package/dist/{PromptTemplateManager-7L3HJQQU.js → PromptTemplateManager-OUYDHOPI.js} +2 -2
- package/dist/README.md +32 -3
- package/dist/{SettingsManager-YU4VYPTW.js → SettingsManager-VCVLL32H.js} +4 -2
- package/dist/{SettingsMigrationManager-KZKDG66H.js → SettingsMigrationManager-LEBMJP3B.js} +3 -3
- package/dist/agents/iloom-code-reviewer.md +720 -0
- package/dist/agents/iloom-issue-analyze-and-plan.md +1 -1
- package/dist/agents/iloom-issue-analyzer.md +1 -1
- package/dist/agents/iloom-issue-complexity-evaluator.md +1 -1
- package/dist/agents/iloom-issue-enhancer.md +1 -1
- package/dist/agents/iloom-issue-implementer.md +1 -1
- package/dist/agents/iloom-issue-planner.md +1 -1
- package/dist/{build-HQ5HGA3T.js → build-H4DK3DMQ.js} +7 -7
- package/dist/{chunk-N7FVXZNI.js → chunk-4BSXZ5YZ.js} +31 -9
- package/dist/chunk-4BSXZ5YZ.js.map +1 -0
- package/dist/{chunk-VYKKWU36.js → chunk-4KGRPHM6.js} +3 -3
- package/dist/{chunk-CFQVOTHO.js → chunk-52MVUK5V.js} +2 -2
- package/dist/{chunk-TIYJEEVO.js → chunk-66QOCD5N.js} +1 -1
- package/dist/chunk-66QOCD5N.js.map +1 -0
- package/dist/chunk-7JDMYTFZ.js +251 -0
- package/dist/chunk-7JDMYTFZ.js.map +1 -0
- package/dist/{chunk-7LSSNB7Y.js → chunk-7ZEHSSUP.js} +2 -2
- package/dist/chunk-A4UQY3M2.js +75 -0
- package/dist/chunk-A4UQY3M2.js.map +1 -0
- package/dist/{chunk-KSXA2NOJ.js → chunk-AZH27CPV.js} +10 -9
- package/dist/chunk-AZH27CPV.js.map +1 -0
- package/dist/{chunk-ELJKYFSH.js → chunk-BCQDYAOJ.js} +4 -4
- package/dist/{chunk-F2PWIRV4.js → chunk-BYUMEDDD.js} +2 -2
- package/dist/{chunk-CAXFWFV6.js → chunk-ECP77QGE.js} +4 -4
- package/dist/{chunk-ZA575VLF.js → chunk-GDS2HXSW.js} +4 -4
- package/dist/{chunk-UDRZY65Y.js → chunk-HSGZW3ID.js} +2 -2
- package/dist/{chunk-WFQ5CLTR.js → chunk-IWIIOFEB.js} +56 -5
- package/dist/chunk-IWIIOFEB.js.map +1 -0
- package/dist/{chunk-VWGKGNJP.js → chunk-KBEIQP4G.js} +3 -1
- package/dist/chunk-KBEIQP4G.js.map +1 -0
- package/dist/{chunk-LZBSLO6S.js → chunk-L4CN7YQT.js} +381 -8
- package/dist/chunk-L4CN7YQT.js.map +1 -0
- package/dist/{chunk-HBJITKSZ.js → chunk-LFVRG6UU.js} +159 -3
- package/dist/chunk-LFVRG6UU.js.map +1 -0
- package/dist/{chunk-64HCHVJM.js → chunk-PLI3JQWT.js} +2 -2
- package/dist/{chunk-USJSNHGG.js → chunk-PVW6JE7E.js} +3 -3
- package/dist/{chunk-3K3WY3BN.js → chunk-QJX6ICWY.js} +4 -4
- package/dist/{chunk-C7YW5IMS.js → chunk-RODL2HVY.js} +17 -6
- package/dist/{chunk-C7YW5IMS.js.map → chunk-RODL2HVY.js.map} +1 -1
- package/dist/{chunk-NEPH2O4C.js → chunk-SSASIBDJ.js} +3 -3
- package/dist/{chunk-GCPAZSGV.js → chunk-THS5L54H.js} +150 -3
- package/dist/chunk-THS5L54H.js.map +1 -0
- package/dist/{chunk-5V74K5ZA.js → chunk-TVH67KEO.js} +25 -2
- package/dist/chunk-TVH67KEO.js.map +1 -0
- package/dist/{chunk-ENMTWE74.js → chunk-VZYSM7N7.js} +2 -2
- package/dist/{chunk-77VLG2KP.js → chunk-WNXYC7J4.js} +18 -16
- package/dist/chunk-WNXYC7J4.js.map +1 -0
- package/dist/{chunk-WZYBHD7P.js → chunk-XHNACIHO.js} +2 -2
- package/dist/{chunk-XAMBIVXE.js → chunk-XJHQVOT6.js} +2 -2
- package/dist/{chunk-O36JLYNW.js → chunk-XU5A6BWA.js} +4 -7
- package/dist/chunk-XU5A6BWA.js.map +1 -0
- package/dist/{chunk-TB6475EW.js → chunk-YAVVDZVF.js} +3 -3
- package/dist/{cleanup-DB7EFBF3.js → cleanup-25PCP2EM.js} +16 -16
- package/dist/cli.js +107 -157
- package/dist/cli.js.map +1 -1
- package/dist/{commit-NGMDWWAP.js → commit-SS77KUNX.js} +10 -10
- package/dist/{compile-CT7IR7O2.js → compile-ZOAODFN2.js} +7 -7
- package/dist/{contribute-GXKOIA42.js → contribute-7USRBWRM.js} +6 -6
- package/dist/{dev-server-OAP3RZC6.js → dev-server-TYYJM3XA.js} +9 -9
- package/dist/{feedback-ZLAX3BVL.js → feedback-HZVLOTQJ.js} +9 -9
- package/dist/{git-ENLT2VNI.js → git-GUNOPP4Q.js} +4 -4
- package/dist/hooks/iloom-hook.js +75 -3
- package/dist/{ignite-HA2OJF6Z.js → ignite-CPXPZ4ZD.js} +85 -25
- package/dist/ignite-CPXPZ4ZD.js.map +1 -0
- package/dist/index.d.ts +85 -2
- package/dist/index.js +133 -73
- package/dist/index.js.map +1 -1
- package/dist/init-MZBIXQ7V.js +21 -0
- package/dist/{lint-HAVU4U34.js → lint-MDVUV3W2.js} +7 -7
- package/dist/mcp/issue-management-server.js +832 -7
- package/dist/mcp/issue-management-server.js.map +1 -1
- package/dist/{neon-helpers-3KBC4A3Y.js → neon-helpers-VVFFTLXE.js} +3 -3
- package/dist/{open-IN3LUZXX.js → open-2LPZ7XXW.js} +9 -9
- package/dist/plan-N3YDCOIV.js +371 -0
- package/dist/plan-N3YDCOIV.js.map +1 -0
- package/dist/{projects-CTRTTMSK.js → projects-325GEEGJ.js} +2 -2
- package/dist/{prompt-3SAZYRUN.js → prompt-ONNPSNKM.js} +2 -2
- package/dist/prompts/init-prompt.txt +57 -1
- package/dist/prompts/issue-prompt.txt +51 -3
- package/dist/prompts/plan-prompt.txt +435 -0
- package/dist/prompts/pr-prompt.txt +38 -0
- package/dist/prompts/regular-prompt.txt +53 -3
- package/dist/{rebase-RLEVFHWN.js → rebase-7YS3N274.js} +6 -6
- package/dist/{recap-ZKGHZCX6.js → recap-GSXFEOD6.js} +6 -6
- package/dist/{run-QEIS2EH2.js → run-XPGCMFFO.js} +9 -9
- package/dist/schema/settings.schema.json +57 -1
- package/dist/{shell-2NNSIU34.js → shell-2SPM3Z5O.js} +6 -6
- package/dist/{summary-2KLNHVTN.js → summary-5UWNLAI5.js} +43 -12
- package/dist/summary-5UWNLAI5.js.map +1 -0
- package/dist/{test-75WAA6DU.js → test-N2725YRI.js} +7 -7
- package/dist/{test-git-E2BLXR6M.js → test-git-ZPSPA2TP.js} +4 -4
- package/dist/{test-prefix-A7JGGYAA.js → test-prefix-6DLB2BHE.js} +4 -4
- package/dist/{test-webserver-J6SMNLU2.js → test-webserver-XLJ2TZFP.js} +6 -6
- package/package.json +1 -1
- package/dist/GitHubService-O7U4UQ7N.js +0 -12
- package/dist/agents/iloom-issue-reviewer.md +0 -139
- package/dist/chunk-5V74K5ZA.js.map +0 -1
- package/dist/chunk-77VLG2KP.js.map +0 -1
- package/dist/chunk-GCPAZSGV.js.map +0 -1
- package/dist/chunk-HBJITKSZ.js.map +0 -1
- package/dist/chunk-KSXA2NOJ.js.map +0 -1
- package/dist/chunk-LZBSLO6S.js.map +0 -1
- package/dist/chunk-N7FVXZNI.js.map +0 -1
- package/dist/chunk-O36JLYNW.js.map +0 -1
- package/dist/chunk-TIYJEEVO.js.map +0 -1
- package/dist/chunk-VWGKGNJP.js.map +0 -1
- package/dist/chunk-WFQ5CLTR.js.map +0 -1
- package/dist/chunk-ZX3GTM7O.js +0 -119
- package/dist/chunk-ZX3GTM7O.js.map +0 -1
- package/dist/ignite-HA2OJF6Z.js.map +0 -1
- package/dist/init-S6IEGRSX.js +0 -21
- package/dist/summary-2KLNHVTN.js.map +0 -1
- /package/dist/{ClaudeContextManager-Y2YJC6BU.js.map → ClaudeContextManager-RDP6CLK6.js.map} +0 -0
- /package/dist/{ClaudeService-NDVFQRKC.js.map → ClaudeService-FKPOQRA4.js.map} +0 -0
- /package/dist/{GitHubService-O7U4UQ7N.js.map → GitHubService-ACZVNTJE.js.map} +0 -0
- /package/dist/{LoomLauncher-U2B3VHPC.js.map → LoomLauncher-NHZMEVTQ.js.map} +0 -0
- /package/dist/{MetadataManager-XJ2YB762.js.map → MetadataManager-W3C54UYT.js.map} +0 -0
- /package/dist/{PRManager-6ZJZRG5Z.js.map → PRManager-XLTVG6YG.js.map} +0 -0
- /package/dist/{PromptTemplateManager-7L3HJQQU.js.map → PromptTemplateManager-OUYDHOPI.js.map} +0 -0
- /package/dist/{SettingsManager-YU4VYPTW.js.map → SettingsManager-VCVLL32H.js.map} +0 -0
- /package/dist/{SettingsMigrationManager-KZKDG66H.js.map → SettingsMigrationManager-LEBMJP3B.js.map} +0 -0
- /package/dist/{build-HQ5HGA3T.js.map → build-H4DK3DMQ.js.map} +0 -0
- /package/dist/{chunk-VYKKWU36.js.map → chunk-4KGRPHM6.js.map} +0 -0
- /package/dist/{chunk-CFQVOTHO.js.map → chunk-52MVUK5V.js.map} +0 -0
- /package/dist/{chunk-7LSSNB7Y.js.map → chunk-7ZEHSSUP.js.map} +0 -0
- /package/dist/{chunk-ELJKYFSH.js.map → chunk-BCQDYAOJ.js.map} +0 -0
- /package/dist/{chunk-F2PWIRV4.js.map → chunk-BYUMEDDD.js.map} +0 -0
- /package/dist/{chunk-CAXFWFV6.js.map → chunk-ECP77QGE.js.map} +0 -0
- /package/dist/{chunk-ZA575VLF.js.map → chunk-GDS2HXSW.js.map} +0 -0
- /package/dist/{chunk-UDRZY65Y.js.map → chunk-HSGZW3ID.js.map} +0 -0
- /package/dist/{chunk-64HCHVJM.js.map → chunk-PLI3JQWT.js.map} +0 -0
- /package/dist/{chunk-USJSNHGG.js.map → chunk-PVW6JE7E.js.map} +0 -0
- /package/dist/{chunk-3K3WY3BN.js.map → chunk-QJX6ICWY.js.map} +0 -0
- /package/dist/{chunk-NEPH2O4C.js.map → chunk-SSASIBDJ.js.map} +0 -0
- /package/dist/{chunk-ENMTWE74.js.map → chunk-VZYSM7N7.js.map} +0 -0
- /package/dist/{chunk-WZYBHD7P.js.map → chunk-XHNACIHO.js.map} +0 -0
- /package/dist/{chunk-XAMBIVXE.js.map → chunk-XJHQVOT6.js.map} +0 -0
- /package/dist/{chunk-TB6475EW.js.map → chunk-YAVVDZVF.js.map} +0 -0
- /package/dist/{cleanup-DB7EFBF3.js.map → cleanup-25PCP2EM.js.map} +0 -0
- /package/dist/{commit-NGMDWWAP.js.map → commit-SS77KUNX.js.map} +0 -0
- /package/dist/{compile-CT7IR7O2.js.map → compile-ZOAODFN2.js.map} +0 -0
- /package/dist/{contribute-GXKOIA42.js.map → contribute-7USRBWRM.js.map} +0 -0
- /package/dist/{dev-server-OAP3RZC6.js.map → dev-server-TYYJM3XA.js.map} +0 -0
- /package/dist/{feedback-ZLAX3BVL.js.map → feedback-HZVLOTQJ.js.map} +0 -0
- /package/dist/{git-ENLT2VNI.js.map → git-GUNOPP4Q.js.map} +0 -0
- /package/dist/{init-S6IEGRSX.js.map → init-MZBIXQ7V.js.map} +0 -0
- /package/dist/{lint-HAVU4U34.js.map → lint-MDVUV3W2.js.map} +0 -0
- /package/dist/{neon-helpers-3KBC4A3Y.js.map → neon-helpers-VVFFTLXE.js.map} +0 -0
- /package/dist/{open-IN3LUZXX.js.map → open-2LPZ7XXW.js.map} +0 -0
- /package/dist/{projects-CTRTTMSK.js.map → projects-325GEEGJ.js.map} +0 -0
- /package/dist/{prompt-3SAZYRUN.js.map → prompt-ONNPSNKM.js.map} +0 -0
- /package/dist/{rebase-RLEVFHWN.js.map → rebase-7YS3N274.js.map} +0 -0
- /package/dist/{recap-ZKGHZCX6.js.map → recap-GSXFEOD6.js.map} +0 -0
- /package/dist/{run-QEIS2EH2.js.map → run-XPGCMFFO.js.map} +0 -0
- /package/dist/{shell-2NNSIU34.js.map → shell-2SPM3Z5O.js.map} +0 -0
- /package/dist/{test-75WAA6DU.js.map → test-N2725YRI.js.map} +0 -0
- /package/dist/{test-git-E2BLXR6M.js.map → test-git-ZPSPA2TP.js.map} +0 -0
- /package/dist/{test-prefix-A7JGGYAA.js.map → test-prefix-6DLB2BHE.js.map} +0 -0
- /package/dist/{test-webserver-J6SMNLU2.js.map → test-webserver-XLJ2TZFP.js.map} +0 -0
|
@@ -125,7 +125,7 @@ async function executeGhCommand(args, options) {
|
|
|
125
125
|
timeout: (options == null ? void 0 : options.timeout) ?? 3e4,
|
|
126
126
|
encoding: "utf8"
|
|
127
127
|
});
|
|
128
|
-
const isJson = args.includes("--json") || args.includes("--jq") || args.includes("--format") && args[args.indexOf("--format") + 1] === "json";
|
|
128
|
+
const isJson = args.includes("--json") || args.includes("--jq") || args.includes("--format") && args[args.indexOf("--format") + 1] === "json" || args[0] === "api" && args[1] === "graphql";
|
|
129
129
|
const data = isJson ? JSON.parse(result.stdout) : result.stdout;
|
|
130
130
|
return data;
|
|
131
131
|
}
|
|
@@ -235,6 +235,386 @@ async function addSubIssue(parentNodeId, childNodeId) {
|
|
|
235
235
|
`subIssueId=${childNodeId}`
|
|
236
236
|
]);
|
|
237
237
|
}
|
|
238
|
+
async function getSubIssues(issueNumber, repo) {
|
|
239
|
+
var _a, _b;
|
|
240
|
+
logger.debug("Fetching GitHub sub-issues", { issueNumber, repo });
|
|
241
|
+
const parentNodeId = await getIssueNodeId(issueNumber, repo);
|
|
242
|
+
const query = `
|
|
243
|
+
query getSubIssues($parentId: ID!) {
|
|
244
|
+
node(id: $parentId) {
|
|
245
|
+
... on Issue {
|
|
246
|
+
subIssues(first: 100) {
|
|
247
|
+
nodes {
|
|
248
|
+
number
|
|
249
|
+
title
|
|
250
|
+
url
|
|
251
|
+
state
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
`;
|
|
258
|
+
try {
|
|
259
|
+
const result = await executeGhCommand([
|
|
260
|
+
"api",
|
|
261
|
+
"graphql",
|
|
262
|
+
"-H",
|
|
263
|
+
"GraphQL-Features: sub_issues",
|
|
264
|
+
"-f",
|
|
265
|
+
`query=${query}`,
|
|
266
|
+
"-F",
|
|
267
|
+
`parentId=${parentNodeId}`
|
|
268
|
+
]);
|
|
269
|
+
const subIssues = ((_b = (_a = result.data.node) == null ? void 0 : _a.subIssues) == null ? void 0 : _b.nodes) ?? [];
|
|
270
|
+
return subIssues.map((issue) => ({
|
|
271
|
+
id: String(issue.number),
|
|
272
|
+
title: issue.title,
|
|
273
|
+
url: issue.url,
|
|
274
|
+
state: issue.state.toLowerCase()
|
|
275
|
+
}));
|
|
276
|
+
} catch (error) {
|
|
277
|
+
if (error instanceof Error) {
|
|
278
|
+
const errorMessage = error.message;
|
|
279
|
+
const stderr = "stderr" in error ? error.stderr ?? "" : "";
|
|
280
|
+
const combinedError = `${errorMessage} ${stderr}`;
|
|
281
|
+
if (combinedError.includes("sub_issues") || combinedError.includes("null")) {
|
|
282
|
+
return [];
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
throw error;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
async function getIssueDatabaseId(issueNumber, repo) {
|
|
289
|
+
logger.debug("Fetching GitHub issue database ID", { issueNumber, repo });
|
|
290
|
+
const apiPath = repo ? `repos/${repo}/issues/${issueNumber}` : `repos/:owner/:repo/issues/${issueNumber}`;
|
|
291
|
+
const result = await executeGhCommand([
|
|
292
|
+
"api",
|
|
293
|
+
apiPath,
|
|
294
|
+
"--jq",
|
|
295
|
+
"{id: .id}"
|
|
296
|
+
]);
|
|
297
|
+
return result.id;
|
|
298
|
+
}
|
|
299
|
+
async function getIssueDependencies(issueNumber, direction, repo) {
|
|
300
|
+
logger.debug("Fetching GitHub issue dependencies", { issueNumber, direction, repo });
|
|
301
|
+
const apiPath = repo ? `repos/${repo}/issues/${issueNumber}/dependencies/${direction}` : `repos/:owner/:repo/issues/${issueNumber}/dependencies/${direction}`;
|
|
302
|
+
try {
|
|
303
|
+
const result = await executeGhCommand([
|
|
304
|
+
"api",
|
|
305
|
+
"-H",
|
|
306
|
+
"Accept: application/vnd.github+json",
|
|
307
|
+
"-H",
|
|
308
|
+
"X-GitHub-Api-Version: 2022-11-28",
|
|
309
|
+
"--jq",
|
|
310
|
+
".",
|
|
311
|
+
apiPath
|
|
312
|
+
]);
|
|
313
|
+
return (result ?? []).map((dep) => ({
|
|
314
|
+
id: String(dep.number),
|
|
315
|
+
databaseId: dep.id,
|
|
316
|
+
title: dep.title,
|
|
317
|
+
url: dep.html_url,
|
|
318
|
+
state: dep.state
|
|
319
|
+
}));
|
|
320
|
+
} catch (error) {
|
|
321
|
+
if (error instanceof Error) {
|
|
322
|
+
const errorMessage = error.message;
|
|
323
|
+
const stderr = "stderr" in error ? error.stderr ?? "" : "";
|
|
324
|
+
const combinedError = `${errorMessage} ${stderr}`;
|
|
325
|
+
if (combinedError.includes("404") && combinedError.includes("dependencies")) {
|
|
326
|
+
return [];
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
throw error;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
async function createIssueDependency(blockedIssueNumber, blockingIssueDatabaseId, repo) {
|
|
333
|
+
logger.debug("Creating GitHub issue dependency", { blockedIssueNumber, blockingIssueDatabaseId, repo });
|
|
334
|
+
const apiPath = repo ? `repos/${repo}/issues/${blockedIssueNumber}/dependencies/blocked_by` : `repos/:owner/:repo/issues/${blockedIssueNumber}/dependencies/blocked_by`;
|
|
335
|
+
try {
|
|
336
|
+
await executeGhCommand([
|
|
337
|
+
"api",
|
|
338
|
+
"-X",
|
|
339
|
+
"POST",
|
|
340
|
+
"-H",
|
|
341
|
+
"Accept: application/vnd.github+json",
|
|
342
|
+
"-H",
|
|
343
|
+
"X-GitHub-Api-Version: 2022-11-28",
|
|
344
|
+
apiPath,
|
|
345
|
+
"-F",
|
|
346
|
+
`issue_id=${blockingIssueDatabaseId}`
|
|
347
|
+
]);
|
|
348
|
+
} catch (error) {
|
|
349
|
+
if (error instanceof Error) {
|
|
350
|
+
const errorMessage = error.message;
|
|
351
|
+
const stderr = "stderr" in error ? error.stderr ?? "" : "";
|
|
352
|
+
const combinedError = `${errorMessage} ${stderr}`;
|
|
353
|
+
if (combinedError.includes("422") || combinedError.includes("already exists") || combinedError.includes("Unprocessable Entity")) {
|
|
354
|
+
throw new Error(`Dependency already exists: issue #${blockedIssueNumber} is already blocked by the specified issue`);
|
|
355
|
+
}
|
|
356
|
+
if (combinedError.includes("404") || combinedError.includes("Not Found")) {
|
|
357
|
+
throw new Error(`Issue not found: unable to create dependency for issue #${blockedIssueNumber}. The issue may not exist or you may not have access to it.`);
|
|
358
|
+
}
|
|
359
|
+
if (combinedError.includes("403") || combinedError.includes("Forbidden") || combinedError.includes("not enabled")) {
|
|
360
|
+
throw new Error(`Dependencies feature not enabled: the repository may not have issue dependencies enabled. This feature requires GitHub Enterprise or specific repository settings.`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
throw error;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
async function removeIssueDependency(blockedIssueNumber, blockingIssueDatabaseId, repo) {
|
|
367
|
+
logger.debug("Removing GitHub issue dependency", { blockedIssueNumber, blockingIssueDatabaseId, repo });
|
|
368
|
+
const apiPath = repo ? `repos/${repo}/issues/${blockedIssueNumber}/dependencies/blocked_by/${blockingIssueDatabaseId}` : `repos/:owner/:repo/issues/${blockedIssueNumber}/dependencies/blocked_by/${blockingIssueDatabaseId}`;
|
|
369
|
+
await executeGhCommand([
|
|
370
|
+
"api",
|
|
371
|
+
"-X",
|
|
372
|
+
"DELETE",
|
|
373
|
+
"-H",
|
|
374
|
+
"Accept: application/vnd.github+json",
|
|
375
|
+
"-H",
|
|
376
|
+
"X-GitHub-Api-Version: 2022-11-28",
|
|
377
|
+
apiPath
|
|
378
|
+
]);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// src/utils/image-processor.ts
|
|
382
|
+
import { tmpdir } from "os";
|
|
383
|
+
import { join, extname } from "path";
|
|
384
|
+
import { existsSync as existsSync2, mkdirSync, createWriteStream, unlinkSync } from "fs";
|
|
385
|
+
import { pipeline } from "stream/promises";
|
|
386
|
+
import { Readable } from "stream";
|
|
387
|
+
import { createHash } from "crypto";
|
|
388
|
+
import { execa as execa3 } from "execa";
|
|
389
|
+
var SUPPORTED_EXTENSIONS = [".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"];
|
|
390
|
+
var MAX_IMAGE_SIZE = 10 * 1024 * 1024;
|
|
391
|
+
var REQUEST_TIMEOUT_MS = 3e4;
|
|
392
|
+
var CACHE_DIR = join(tmpdir(), "iloom-images");
|
|
393
|
+
var cachedGitHubToken;
|
|
394
|
+
function extractMarkdownImageUrls(content) {
|
|
395
|
+
if (!content) {
|
|
396
|
+
return [];
|
|
397
|
+
}
|
|
398
|
+
const matches = [];
|
|
399
|
+
const markdownRegex = /!\[([^\]]*)\]\(((?:[^()\s]|\((?:[^()\s]|\([^()]*\))*\))+)\)/g;
|
|
400
|
+
let match;
|
|
401
|
+
while ((match = markdownRegex.exec(content)) !== null) {
|
|
402
|
+
const url = match[2];
|
|
403
|
+
if (url) {
|
|
404
|
+
matches.push({
|
|
405
|
+
fullMatch: match[0],
|
|
406
|
+
url,
|
|
407
|
+
isMarkdown: true
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
const htmlImgRegex = /<img\s+[^>]*src=["']([^"']+)["'][^>]*\/?>/gi;
|
|
412
|
+
while ((match = htmlImgRegex.exec(content)) !== null) {
|
|
413
|
+
const url = match[1];
|
|
414
|
+
if (url) {
|
|
415
|
+
matches.push({
|
|
416
|
+
fullMatch: match[0],
|
|
417
|
+
url,
|
|
418
|
+
isMarkdown: false
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return matches;
|
|
423
|
+
}
|
|
424
|
+
function isAuthenticatedImageUrl(url) {
|
|
425
|
+
try {
|
|
426
|
+
const parsedUrl = new URL(url);
|
|
427
|
+
const hostname = parsedUrl.hostname.toLowerCase();
|
|
428
|
+
if (hostname === "uploads.linear.app") {
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
431
|
+
if (hostname === "private-user-images.githubusercontent.com") {
|
|
432
|
+
return true;
|
|
433
|
+
}
|
|
434
|
+
if (hostname === "github.com" && parsedUrl.pathname.startsWith("/user-attachments/assets/")) {
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
return false;
|
|
438
|
+
} catch {
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
function getExtensionFromUrl(url) {
|
|
443
|
+
try {
|
|
444
|
+
const parsedUrl = new URL(url);
|
|
445
|
+
const pathname = parsedUrl.pathname;
|
|
446
|
+
const ext = extname(pathname).toLowerCase();
|
|
447
|
+
if (SUPPORTED_EXTENSIONS.includes(ext)) {
|
|
448
|
+
return ext;
|
|
449
|
+
}
|
|
450
|
+
return null;
|
|
451
|
+
} catch {
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
function getCacheKey(url) {
|
|
456
|
+
const parsedUrl = new URL(url);
|
|
457
|
+
if (parsedUrl.hostname === "private-user-images.githubusercontent.com") {
|
|
458
|
+
parsedUrl.searchParams.delete("jwt");
|
|
459
|
+
}
|
|
460
|
+
const stableUrl = parsedUrl.toString();
|
|
461
|
+
const hash = createHash("sha256").update(stableUrl).digest("hex").slice(0, 16);
|
|
462
|
+
const ext = getExtensionFromUrl(url) ?? ".png";
|
|
463
|
+
return `${hash}${ext}`;
|
|
464
|
+
}
|
|
465
|
+
function getCachedImagePath(url) {
|
|
466
|
+
const cacheKey = getCacheKey(url);
|
|
467
|
+
const cachedPath = join(CACHE_DIR, cacheKey);
|
|
468
|
+
if (existsSync2(cachedPath)) {
|
|
469
|
+
return cachedPath;
|
|
470
|
+
}
|
|
471
|
+
return void 0;
|
|
472
|
+
}
|
|
473
|
+
async function getAuthToken(provider) {
|
|
474
|
+
if (provider === "github") {
|
|
475
|
+
if (cachedGitHubToken !== void 0) {
|
|
476
|
+
return cachedGitHubToken;
|
|
477
|
+
}
|
|
478
|
+
try {
|
|
479
|
+
const result = await execa3("gh", ["auth", "token"]);
|
|
480
|
+
cachedGitHubToken = result.stdout.trim();
|
|
481
|
+
return cachedGitHubToken;
|
|
482
|
+
} catch (error) {
|
|
483
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
484
|
+
logger.warn(`Failed to get GitHub auth token via gh CLI: ${message}`);
|
|
485
|
+
return void 0;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (provider === "linear") {
|
|
489
|
+
return process.env.LINEAR_API_TOKEN;
|
|
490
|
+
}
|
|
491
|
+
return void 0;
|
|
492
|
+
}
|
|
493
|
+
async function downloadAndSaveImage(url, destPath, authHeader) {
|
|
494
|
+
const headers = {};
|
|
495
|
+
if (authHeader) {
|
|
496
|
+
headers["Authorization"] = authHeader;
|
|
497
|
+
}
|
|
498
|
+
const controller = new AbortController();
|
|
499
|
+
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
500
|
+
try {
|
|
501
|
+
const response = await fetch(url, { headers, signal: controller.signal });
|
|
502
|
+
if (!response.ok) {
|
|
503
|
+
throw new Error(`Failed to download image: ${response.status} ${response.statusText}`);
|
|
504
|
+
}
|
|
505
|
+
const contentLength = response.headers.get("Content-Length");
|
|
506
|
+
if (contentLength && parseInt(contentLength, 10) > MAX_IMAGE_SIZE) {
|
|
507
|
+
throw new Error(`Image too large: ${contentLength} bytes exceeds ${MAX_IMAGE_SIZE} byte limit`);
|
|
508
|
+
}
|
|
509
|
+
if (!response.body) {
|
|
510
|
+
throw new Error("Response body is null");
|
|
511
|
+
}
|
|
512
|
+
const reader = response.body.getReader();
|
|
513
|
+
let bytesWritten = 0;
|
|
514
|
+
const nodeReadable = new Readable({
|
|
515
|
+
async read() {
|
|
516
|
+
try {
|
|
517
|
+
const { done, value } = await reader.read();
|
|
518
|
+
if (done) {
|
|
519
|
+
this.push(null);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
bytesWritten += value.byteLength;
|
|
523
|
+
if (bytesWritten > MAX_IMAGE_SIZE) {
|
|
524
|
+
reader.cancel();
|
|
525
|
+
this.destroy(new Error(`Image too large: ${bytesWritten} bytes exceeds ${MAX_IMAGE_SIZE} byte limit`));
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
this.push(Buffer.from(value));
|
|
529
|
+
} catch (err) {
|
|
530
|
+
this.destroy(err instanceof Error ? err : new Error(String(err)));
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
if (!existsSync2(CACHE_DIR)) {
|
|
535
|
+
mkdirSync(CACHE_DIR, { recursive: true });
|
|
536
|
+
}
|
|
537
|
+
const writeStream = createWriteStream(destPath);
|
|
538
|
+
try {
|
|
539
|
+
await pipeline(nodeReadable, writeStream);
|
|
540
|
+
} catch (pipelineError) {
|
|
541
|
+
try {
|
|
542
|
+
if (existsSync2(destPath)) {
|
|
543
|
+
unlinkSync(destPath);
|
|
544
|
+
}
|
|
545
|
+
} catch {
|
|
546
|
+
}
|
|
547
|
+
throw pipelineError;
|
|
548
|
+
}
|
|
549
|
+
} catch (error) {
|
|
550
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
551
|
+
throw new Error(`Image download timed out after ${REQUEST_TIMEOUT_MS}ms`);
|
|
552
|
+
}
|
|
553
|
+
throw error;
|
|
554
|
+
} finally {
|
|
555
|
+
clearTimeout(timeoutId);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
function getCacheDestPath(url) {
|
|
559
|
+
if (!existsSync2(CACHE_DIR)) {
|
|
560
|
+
mkdirSync(CACHE_DIR, { recursive: true });
|
|
561
|
+
}
|
|
562
|
+
const cacheKey = getCacheKey(url);
|
|
563
|
+
return join(CACHE_DIR, cacheKey);
|
|
564
|
+
}
|
|
565
|
+
function rewriteMarkdownUrls(content, urlMap) {
|
|
566
|
+
let result = content;
|
|
567
|
+
for (const [originalUrl, localPath] of urlMap) {
|
|
568
|
+
const escapedUrl = originalUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
569
|
+
const urlRegex = new RegExp(escapedUrl, "g");
|
|
570
|
+
result = result.replace(urlRegex, localPath);
|
|
571
|
+
}
|
|
572
|
+
return result;
|
|
573
|
+
}
|
|
574
|
+
async function processMarkdownImages(content, provider) {
|
|
575
|
+
if (!content) {
|
|
576
|
+
return "";
|
|
577
|
+
}
|
|
578
|
+
const images = extractMarkdownImageUrls(content);
|
|
579
|
+
if (images.length === 0) {
|
|
580
|
+
return content;
|
|
581
|
+
}
|
|
582
|
+
const authImages = images.filter((img) => isAuthenticatedImageUrl(img.url));
|
|
583
|
+
if (authImages.length === 0) {
|
|
584
|
+
return content;
|
|
585
|
+
}
|
|
586
|
+
const authToken = await getAuthToken(provider);
|
|
587
|
+
const uniqueUrls = [...new Set(authImages.map((img) => img.url))];
|
|
588
|
+
const urlMap = /* @__PURE__ */ new Map();
|
|
589
|
+
const downloadPromises = uniqueUrls.map(async (url) => {
|
|
590
|
+
try {
|
|
591
|
+
const cachedPath = getCachedImagePath(url);
|
|
592
|
+
if (cachedPath) {
|
|
593
|
+
logger.debug(`Using cached image: ${cachedPath}`);
|
|
594
|
+
return { url, localPath: cachedPath };
|
|
595
|
+
}
|
|
596
|
+
logger.debug(`Downloading image: ${url}`);
|
|
597
|
+
const destPath = getCacheDestPath(url);
|
|
598
|
+
await downloadAndSaveImage(
|
|
599
|
+
url,
|
|
600
|
+
destPath,
|
|
601
|
+
authToken ? `Bearer ${authToken}` : void 0
|
|
602
|
+
);
|
|
603
|
+
return { url, localPath: destPath };
|
|
604
|
+
} catch (error) {
|
|
605
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
606
|
+
logger.warn(`Failed to download image ${url}: ${message}`);
|
|
607
|
+
return null;
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
const results = await Promise.all(downloadPromises);
|
|
611
|
+
for (const result of results) {
|
|
612
|
+
if (result !== null) {
|
|
613
|
+
urlMap.set(result.url, result.localPath);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return rewriteMarkdownUrls(content, urlMap);
|
|
617
|
+
}
|
|
238
618
|
|
|
239
619
|
// src/mcp/GitHubIssueManagementProvider.ts
|
|
240
620
|
function normalizeAuthor(author) {
|
|
@@ -314,6 +694,12 @@ var GitHubIssueManagementProvider = class {
|
|
|
314
694
|
...comment.updatedAt && { updatedAt: comment.updatedAt }
|
|
315
695
|
}));
|
|
316
696
|
}
|
|
697
|
+
result.body = await processMarkdownImages(result.body, "github");
|
|
698
|
+
if (result.comments) {
|
|
699
|
+
for (const comment of result.comments) {
|
|
700
|
+
comment.body = await processMarkdownImages(comment.body, "github");
|
|
701
|
+
}
|
|
702
|
+
}
|
|
317
703
|
return result;
|
|
318
704
|
}
|
|
319
705
|
/**
|
|
@@ -382,6 +768,12 @@ var GitHubIssueManagementProvider = class {
|
|
|
382
768
|
...comment.updatedAt && { updatedAt: comment.updatedAt }
|
|
383
769
|
}));
|
|
384
770
|
}
|
|
771
|
+
result.body = await processMarkdownImages(result.body, "github");
|
|
772
|
+
if (result.comments) {
|
|
773
|
+
for (const comment of result.comments) {
|
|
774
|
+
comment.body = await processMarkdownImages(comment.body, "github");
|
|
775
|
+
}
|
|
776
|
+
}
|
|
385
777
|
return result;
|
|
386
778
|
}
|
|
387
779
|
/**
|
|
@@ -401,9 +793,10 @@ var GitHubIssueManagementProvider = class {
|
|
|
401
793
|
"--jq",
|
|
402
794
|
"{id: .id, body: .body, user: .user, created_at: .created_at, updated_at: .updated_at, html_url: .html_url, reactions: .reactions}"
|
|
403
795
|
]);
|
|
796
|
+
const processedBody = await processMarkdownImages(raw.body, "github");
|
|
404
797
|
return {
|
|
405
798
|
id: String(raw.id),
|
|
406
|
-
body:
|
|
799
|
+
body: processedBody,
|
|
407
800
|
author: normalizeAuthor(raw.user),
|
|
408
801
|
created_at: raw.created_at,
|
|
409
802
|
...raw.updated_at && { updated_at: raw.updated_at },
|
|
@@ -476,10 +869,76 @@ var GitHubIssueManagementProvider = class {
|
|
|
476
869
|
number: childNumber
|
|
477
870
|
};
|
|
478
871
|
}
|
|
872
|
+
/**
|
|
873
|
+
* Create a blocking dependency between two issues (A blocks B)
|
|
874
|
+
* Uses GitHub's sub-issues API: blocking issue becomes parent, blocked issue becomes sub-issue
|
|
875
|
+
*/
|
|
876
|
+
async createDependency(input) {
|
|
877
|
+
const { blockingIssue, blockedIssue, repo } = input;
|
|
878
|
+
const blockingNumber = parseInt(blockingIssue, 10);
|
|
879
|
+
if (isNaN(blockingNumber)) {
|
|
880
|
+
throw new Error(`Invalid GitHub issue number: ${blockingIssue}. GitHub issue IDs must be numeric.`);
|
|
881
|
+
}
|
|
882
|
+
const blockedNumber = parseInt(blockedIssue, 10);
|
|
883
|
+
if (isNaN(blockedNumber)) {
|
|
884
|
+
throw new Error(`Invalid GitHub issue number: ${blockedIssue}. GitHub issue IDs must be numeric.`);
|
|
885
|
+
}
|
|
886
|
+
const blockingDatabaseId = await getIssueDatabaseId(blockingNumber, repo);
|
|
887
|
+
await createIssueDependency(blockedNumber, blockingDatabaseId, repo);
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Get dependencies for an issue
|
|
891
|
+
*/
|
|
892
|
+
async getDependencies(input) {
|
|
893
|
+
const { number, direction, repo } = input;
|
|
894
|
+
const issueNumber = parseInt(number, 10);
|
|
895
|
+
if (isNaN(issueNumber)) {
|
|
896
|
+
throw new Error(`Invalid GitHub issue number: ${number}. GitHub issue IDs must be numeric.`);
|
|
897
|
+
}
|
|
898
|
+
const result = {
|
|
899
|
+
blocking: [],
|
|
900
|
+
blockedBy: []
|
|
901
|
+
};
|
|
902
|
+
if (direction === "blocking" || direction === "both") {
|
|
903
|
+
result.blocking = await getIssueDependencies(issueNumber, "blocking", repo);
|
|
904
|
+
}
|
|
905
|
+
if (direction === "blocked_by" || direction === "both") {
|
|
906
|
+
result.blockedBy = await getIssueDependencies(issueNumber, "blocked_by", repo);
|
|
907
|
+
}
|
|
908
|
+
return result;
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Remove a blocking dependency between two issues (A blocks B)
|
|
912
|
+
* Uses GitHub's sub-issues API: blocking issue is parent, blocked issue is sub-issue
|
|
913
|
+
*/
|
|
914
|
+
async removeDependency(input) {
|
|
915
|
+
const { blockingIssue, blockedIssue, repo } = input;
|
|
916
|
+
const blockingNumber = parseInt(blockingIssue, 10);
|
|
917
|
+
if (isNaN(blockingNumber)) {
|
|
918
|
+
throw new Error(`Invalid GitHub issue number: ${blockingIssue}. GitHub issue IDs must be numeric.`);
|
|
919
|
+
}
|
|
920
|
+
const blockedNumber = parseInt(blockedIssue, 10);
|
|
921
|
+
if (isNaN(blockedNumber)) {
|
|
922
|
+
throw new Error(`Invalid GitHub issue number: ${blockedIssue}. GitHub issue IDs must be numeric.`);
|
|
923
|
+
}
|
|
924
|
+
const blockingDatabaseId = await getIssueDatabaseId(blockingNumber, repo);
|
|
925
|
+
await removeIssueDependency(blockedNumber, blockingDatabaseId, repo);
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Get child issues (sub-issues) of a parent issue
|
|
929
|
+
*/
|
|
930
|
+
async getChildIssues(input) {
|
|
931
|
+
const { number, repo } = input;
|
|
932
|
+
const issueNumber = parseInt(number, 10);
|
|
933
|
+
if (isNaN(issueNumber)) {
|
|
934
|
+
throw new Error(`Invalid GitHub issue number: ${number}. GitHub issue IDs must be numeric.`);
|
|
935
|
+
}
|
|
936
|
+
return await getSubIssues(issueNumber, repo);
|
|
937
|
+
}
|
|
479
938
|
};
|
|
480
939
|
|
|
481
940
|
// src/utils/linear.ts
|
|
482
|
-
import { LinearClient } from "@linear/sdk";
|
|
941
|
+
import { LinearClient, IssueRelationType } from "@linear/sdk";
|
|
483
942
|
|
|
484
943
|
// src/types/linear.ts
|
|
485
944
|
var LinearServiceError = class _LinearServiceError extends Error {
|
|
@@ -749,10 +1208,161 @@ async function fetchLinearIssueComments(identifier) {
|
|
|
749
1208
|
handleLinearError(error, "fetchLinearIssueComments");
|
|
750
1209
|
}
|
|
751
1210
|
}
|
|
1211
|
+
async function getLinearChildIssues(identifier) {
|
|
1212
|
+
try {
|
|
1213
|
+
logger.debug(`Fetching child issues for Linear issue: ${identifier}`);
|
|
1214
|
+
const client = createLinearClient();
|
|
1215
|
+
const issue = await client.issue(identifier);
|
|
1216
|
+
if (!issue) {
|
|
1217
|
+
throw new LinearServiceError("NOT_FOUND", `Linear issue ${identifier} not found`);
|
|
1218
|
+
}
|
|
1219
|
+
const children = await issue.children({ first: 100 });
|
|
1220
|
+
const results = await Promise.all(
|
|
1221
|
+
children.nodes.map(async (child) => {
|
|
1222
|
+
const stateObj = await child.state;
|
|
1223
|
+
const state = (stateObj == null ? void 0 : stateObj.name) ?? "unknown";
|
|
1224
|
+
return {
|
|
1225
|
+
id: child.identifier,
|
|
1226
|
+
title: child.title,
|
|
1227
|
+
url: child.url,
|
|
1228
|
+
state
|
|
1229
|
+
};
|
|
1230
|
+
})
|
|
1231
|
+
);
|
|
1232
|
+
return results;
|
|
1233
|
+
} catch (error) {
|
|
1234
|
+
if (error instanceof LinearServiceError) {
|
|
1235
|
+
throw error;
|
|
1236
|
+
}
|
|
1237
|
+
handleLinearError(error, "getLinearChildIssues");
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
async function createLinearIssueRelation(blockingIssueId, blockedIssueId) {
|
|
1241
|
+
try {
|
|
1242
|
+
logger.debug(`Creating Linear issue relation: ${blockingIssueId} blocks ${blockedIssueId}`);
|
|
1243
|
+
const client = createLinearClient();
|
|
1244
|
+
const payload = await client.createIssueRelation({
|
|
1245
|
+
issueId: blockingIssueId,
|
|
1246
|
+
relatedIssueId: blockedIssueId,
|
|
1247
|
+
type: IssueRelationType.Blocks
|
|
1248
|
+
});
|
|
1249
|
+
if (!payload.success) {
|
|
1250
|
+
throw new LinearServiceError("CLI_ERROR", "Failed to create Linear issue relation");
|
|
1251
|
+
}
|
|
1252
|
+
} catch (error) {
|
|
1253
|
+
if (error instanceof LinearServiceError) {
|
|
1254
|
+
throw error;
|
|
1255
|
+
}
|
|
1256
|
+
handleLinearError(error, "createLinearIssueRelation");
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
async function getLinearIssueDependencies(identifier, direction) {
|
|
1260
|
+
try {
|
|
1261
|
+
logger.debug(`Fetching Linear issue dependencies: ${identifier} (direction: ${direction})`);
|
|
1262
|
+
const client = createLinearClient();
|
|
1263
|
+
const issue = await client.issue(identifier);
|
|
1264
|
+
if (!issue) {
|
|
1265
|
+
throw new LinearServiceError("NOT_FOUND", `Linear issue ${identifier} not found`);
|
|
1266
|
+
}
|
|
1267
|
+
const [relations, inverseRelations] = await Promise.all([
|
|
1268
|
+
issue.relations(),
|
|
1269
|
+
issue.inverseRelations()
|
|
1270
|
+
]);
|
|
1271
|
+
const blocking = [];
|
|
1272
|
+
const blockedBy = [];
|
|
1273
|
+
const buildDependencyResult = async (relatedIssue) => {
|
|
1274
|
+
if (!relatedIssue) return null;
|
|
1275
|
+
const stateObj = await relatedIssue.state;
|
|
1276
|
+
const state = (stateObj == null ? void 0 : stateObj.name) ?? "unknown";
|
|
1277
|
+
return {
|
|
1278
|
+
id: relatedIssue.identifier,
|
|
1279
|
+
title: relatedIssue.title,
|
|
1280
|
+
url: relatedIssue.url,
|
|
1281
|
+
state
|
|
1282
|
+
};
|
|
1283
|
+
};
|
|
1284
|
+
if (direction === "blocking" || direction === "both") {
|
|
1285
|
+
const blockingRelations = relations.nodes.filter(
|
|
1286
|
+
(r) => r.type === IssueRelationType.Blocks
|
|
1287
|
+
);
|
|
1288
|
+
const relatedIssuePromises = blockingRelations.map((r) => r.relatedIssue).filter((p) => p !== void 0);
|
|
1289
|
+
const relatedIssues = await Promise.all(relatedIssuePromises);
|
|
1290
|
+
const blockingResults = await Promise.all(
|
|
1291
|
+
relatedIssues.map((issue2) => buildDependencyResult(issue2))
|
|
1292
|
+
);
|
|
1293
|
+
blocking.push(...blockingResults.filter((r) => r !== null));
|
|
1294
|
+
}
|
|
1295
|
+
if (direction === "blocked_by" || direction === "both") {
|
|
1296
|
+
const blockedByRelations = inverseRelations.nodes.filter(
|
|
1297
|
+
(r) => r.type === IssueRelationType.Blocks
|
|
1298
|
+
);
|
|
1299
|
+
const sourceIssuePromises = blockedByRelations.map((r) => r.issue).filter((p) => p !== void 0);
|
|
1300
|
+
const sourceIssues = await Promise.all(sourceIssuePromises);
|
|
1301
|
+
const blockedByResults = await Promise.all(
|
|
1302
|
+
sourceIssues.map((issue2) => buildDependencyResult(issue2))
|
|
1303
|
+
);
|
|
1304
|
+
blockedBy.push(...blockedByResults.filter((r) => r !== null));
|
|
1305
|
+
}
|
|
1306
|
+
return { blocking, blockedBy };
|
|
1307
|
+
} catch (error) {
|
|
1308
|
+
if (error instanceof LinearServiceError) {
|
|
1309
|
+
throw error;
|
|
1310
|
+
}
|
|
1311
|
+
handleLinearError(error, "getLinearIssueDependencies");
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
async function deleteLinearIssueRelation(relationId) {
|
|
1315
|
+
try {
|
|
1316
|
+
logger.debug(`Deleting Linear issue relation: ${relationId}`);
|
|
1317
|
+
const client = createLinearClient();
|
|
1318
|
+
const payload = await client.deleteIssueRelation(relationId);
|
|
1319
|
+
if (!payload.success) {
|
|
1320
|
+
throw new LinearServiceError("CLI_ERROR", "Failed to delete Linear issue relation");
|
|
1321
|
+
}
|
|
1322
|
+
} catch (error) {
|
|
1323
|
+
if (error instanceof LinearServiceError) {
|
|
1324
|
+
throw error;
|
|
1325
|
+
}
|
|
1326
|
+
handleLinearError(error, "deleteLinearIssueRelation");
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
async function findLinearIssueRelation(blockingIdentifier, blockedIdentifier) {
|
|
1330
|
+
try {
|
|
1331
|
+
logger.debug(`Finding Linear issue relation: ${blockingIdentifier} blocks ${blockedIdentifier}`);
|
|
1332
|
+
const client = createLinearClient();
|
|
1333
|
+
const blockingIssue = await client.issue(blockingIdentifier);
|
|
1334
|
+
if (!blockingIssue) {
|
|
1335
|
+
throw new LinearServiceError("NOT_FOUND", `Linear issue ${blockingIdentifier} not found`);
|
|
1336
|
+
}
|
|
1337
|
+
const blockedIssue = await client.issue(blockedIdentifier);
|
|
1338
|
+
if (!blockedIssue) {
|
|
1339
|
+
throw new LinearServiceError("NOT_FOUND", `Linear issue ${blockedIdentifier} not found`);
|
|
1340
|
+
}
|
|
1341
|
+
const relations = await blockingIssue.relations();
|
|
1342
|
+
const blockingRelations = relations.nodes.filter(
|
|
1343
|
+
(r) => r.type === IssueRelationType.Blocks
|
|
1344
|
+
);
|
|
1345
|
+
const relationsWithIssues = await Promise.all(
|
|
1346
|
+
blockingRelations.map(async (relation) => ({
|
|
1347
|
+
relation,
|
|
1348
|
+
relatedIssue: await relation.relatedIssue
|
|
1349
|
+
}))
|
|
1350
|
+
);
|
|
1351
|
+
const matchingRelation = relationsWithIssues.find(
|
|
1352
|
+
({ relatedIssue }) => (relatedIssue == null ? void 0 : relatedIssue.id) === blockedIssue.id
|
|
1353
|
+
);
|
|
1354
|
+
return (matchingRelation == null ? void 0 : matchingRelation.relation.id) ?? null;
|
|
1355
|
+
} catch (error) {
|
|
1356
|
+
if (error instanceof LinearServiceError) {
|
|
1357
|
+
throw error;
|
|
1358
|
+
}
|
|
1359
|
+
handleLinearError(error, "findLinearIssueRelation");
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
752
1362
|
|
|
753
1363
|
// src/utils/linear-markup-converter.ts
|
|
754
1364
|
import { appendFileSync } from "fs";
|
|
755
|
-
import { join, dirname, basename, extname } from "path";
|
|
1365
|
+
import { join as join2, dirname, basename, extname as extname2 } from "path";
|
|
756
1366
|
var LinearMarkupConverter = class {
|
|
757
1367
|
/**
|
|
758
1368
|
* Convert HTML details/summary blocks to Linear's collapsible format
|
|
@@ -888,7 +1498,7 @@ ${content}
|
|
|
888
1498
|
*/
|
|
889
1499
|
static getTimestampedLogPath(logFilePath) {
|
|
890
1500
|
const dir = dirname(logFilePath);
|
|
891
|
-
const ext =
|
|
1501
|
+
const ext = extname2(logFilePath);
|
|
892
1502
|
const base = basename(logFilePath, ext);
|
|
893
1503
|
const now = /* @__PURE__ */ new Date();
|
|
894
1504
|
const timestamp = [
|
|
@@ -900,7 +1510,7 @@ ${content}
|
|
|
900
1510
|
String(now.getMinutes()).padStart(2, "0"),
|
|
901
1511
|
String(now.getSeconds()).padStart(2, "0")
|
|
902
1512
|
].join("");
|
|
903
|
-
return
|
|
1513
|
+
return join2(dir, `${base}-${timestamp}${ext}`);
|
|
904
1514
|
}
|
|
905
1515
|
};
|
|
906
1516
|
|
|
@@ -949,6 +1559,12 @@ var LinearIssueManagementProvider = class {
|
|
|
949
1559
|
} catch {
|
|
950
1560
|
}
|
|
951
1561
|
}
|
|
1562
|
+
result.body = await processMarkdownImages(result.body, "linear");
|
|
1563
|
+
if (result.comments) {
|
|
1564
|
+
for (const comment of result.comments) {
|
|
1565
|
+
comment.body = await processMarkdownImages(comment.body, "linear");
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
952
1568
|
return result;
|
|
953
1569
|
}
|
|
954
1570
|
/**
|
|
@@ -982,9 +1598,10 @@ var LinearIssueManagementProvider = class {
|
|
|
982
1598
|
async getComment(input) {
|
|
983
1599
|
const { commentId } = input;
|
|
984
1600
|
const raw = await getLinearComment(commentId);
|
|
1601
|
+
const processedBody = await processMarkdownImages(raw.body, "linear");
|
|
985
1602
|
return {
|
|
986
1603
|
id: raw.id,
|
|
987
|
-
body:
|
|
1604
|
+
body: processedBody,
|
|
988
1605
|
author: null,
|
|
989
1606
|
// Linear SDK doesn't return comment author info in basic fetch
|
|
990
1607
|
created_at: raw.createdAt
|
|
@@ -1057,6 +1674,42 @@ var LinearIssueManagementProvider = class {
|
|
|
1057
1674
|
url: result.url
|
|
1058
1675
|
};
|
|
1059
1676
|
}
|
|
1677
|
+
/**
|
|
1678
|
+
* Create a blocking dependency between two issues
|
|
1679
|
+
*/
|
|
1680
|
+
async createDependency(input) {
|
|
1681
|
+
const { blockingIssue, blockedIssue } = input;
|
|
1682
|
+
const [blockingIssueData, blockedIssueData] = await Promise.all([
|
|
1683
|
+
fetchLinearIssue(blockingIssue),
|
|
1684
|
+
fetchLinearIssue(blockedIssue)
|
|
1685
|
+
]);
|
|
1686
|
+
await createLinearIssueRelation(blockingIssueData.id, blockedIssueData.id);
|
|
1687
|
+
}
|
|
1688
|
+
/**
|
|
1689
|
+
* Get dependencies for an issue
|
|
1690
|
+
*/
|
|
1691
|
+
async getDependencies(input) {
|
|
1692
|
+
const { number, direction } = input;
|
|
1693
|
+
return await getLinearIssueDependencies(number, direction);
|
|
1694
|
+
}
|
|
1695
|
+
/**
|
|
1696
|
+
* Remove a blocking dependency between two issues
|
|
1697
|
+
*/
|
|
1698
|
+
async removeDependency(input) {
|
|
1699
|
+
const { blockingIssue, blockedIssue } = input;
|
|
1700
|
+
const relationId = await findLinearIssueRelation(blockingIssue, blockedIssue);
|
|
1701
|
+
if (!relationId) {
|
|
1702
|
+
throw new Error(`No blocking dependency found from ${blockingIssue} to ${blockedIssue}`);
|
|
1703
|
+
}
|
|
1704
|
+
await deleteLinearIssueRelation(relationId);
|
|
1705
|
+
}
|
|
1706
|
+
/**
|
|
1707
|
+
* Get child issues of a parent issue
|
|
1708
|
+
*/
|
|
1709
|
+
async getChildIssues(input) {
|
|
1710
|
+
const { number } = input;
|
|
1711
|
+
return await getLinearChildIssues(number);
|
|
1712
|
+
}
|
|
1060
1713
|
};
|
|
1061
1714
|
|
|
1062
1715
|
// src/mcp/IssueManagementProviderFactory.ts
|
|
@@ -1471,6 +2124,178 @@ server.registerTool(
|
|
|
1471
2124
|
}
|
|
1472
2125
|
}
|
|
1473
2126
|
);
|
|
2127
|
+
var dependencyResultSchema = z.object({
|
|
2128
|
+
id: z.string().describe("Issue identifier"),
|
|
2129
|
+
title: z.string().describe("Issue title"),
|
|
2130
|
+
url: z.string().describe("Issue URL"),
|
|
2131
|
+
state: z.string().describe("Issue state")
|
|
2132
|
+
});
|
|
2133
|
+
server.registerTool(
|
|
2134
|
+
"create_dependency",
|
|
2135
|
+
{
|
|
2136
|
+
title: "Create Dependency",
|
|
2137
|
+
description: 'Create a blocking dependency between two issues. The blockingIssue will block the blockedIssue. For GitHub: uses the sub-issue API. For Linear: creates a "blocks" relation.',
|
|
2138
|
+
inputSchema: {
|
|
2139
|
+
blockingIssue: z.string().describe('The issue that blocks (GitHub issue number or Linear identifier like "ENG-123")'),
|
|
2140
|
+
blockedIssue: z.string().describe('The issue being blocked (GitHub issue number or Linear identifier like "ENG-123")'),
|
|
2141
|
+
repo: z.string().optional().describe(
|
|
2142
|
+
'Optional repository in "owner/repo" format or full GitHub URL. When not provided, uses the current repository. GitHub only.'
|
|
2143
|
+
)
|
|
2144
|
+
},
|
|
2145
|
+
outputSchema: {
|
|
2146
|
+
success: z.boolean().describe("Whether the dependency was created successfully")
|
|
2147
|
+
}
|
|
2148
|
+
},
|
|
2149
|
+
async ({ blockingIssue, blockedIssue, repo }) => {
|
|
2150
|
+
console.error(`Creating dependency: ${blockingIssue} blocks ${blockedIssue}${repo ? ` in ${repo}` : ""}`);
|
|
2151
|
+
try {
|
|
2152
|
+
const provider = IssueManagementProviderFactory.create(
|
|
2153
|
+
process.env.ISSUE_PROVIDER
|
|
2154
|
+
);
|
|
2155
|
+
await provider.createDependency({ blockingIssue, blockedIssue, repo });
|
|
2156
|
+
console.error(`Dependency created successfully: ${blockingIssue} -> ${blockedIssue}`);
|
|
2157
|
+
return {
|
|
2158
|
+
content: [
|
|
2159
|
+
{
|
|
2160
|
+
type: "text",
|
|
2161
|
+
text: JSON.stringify({ success: true })
|
|
2162
|
+
}
|
|
2163
|
+
],
|
|
2164
|
+
structuredContent: { success: true }
|
|
2165
|
+
};
|
|
2166
|
+
} catch (error) {
|
|
2167
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
2168
|
+
console.error(`Failed to create dependency: ${errorMessage}`);
|
|
2169
|
+
throw new Error(`Failed to create dependency: ${errorMessage}`);
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
);
|
|
2173
|
+
server.registerTool(
|
|
2174
|
+
"get_dependencies",
|
|
2175
|
+
{
|
|
2176
|
+
title: "Get Dependencies",
|
|
2177
|
+
description: "Get blocking/blocked_by dependencies for an issue. Returns lists of issues that this issue blocks and/or is blocked by.",
|
|
2178
|
+
inputSchema: {
|
|
2179
|
+
number: z.string().describe('Issue identifier (GitHub issue number or Linear identifier like "ENG-123")'),
|
|
2180
|
+
direction: z.enum(["blocking", "blocked_by", "both"]).describe('Which dependencies to fetch: "blocking" for issues this blocks, "blocked_by" for issues blocking this, "both" for all'),
|
|
2181
|
+
repo: z.string().optional().describe(
|
|
2182
|
+
'Optional repository in "owner/repo" format or full GitHub URL. When not provided, uses the current repository. GitHub only.'
|
|
2183
|
+
)
|
|
2184
|
+
},
|
|
2185
|
+
outputSchema: {
|
|
2186
|
+
blocking: z.array(dependencyResultSchema).describe("Issues that this issue blocks"),
|
|
2187
|
+
blockedBy: z.array(dependencyResultSchema).describe("Issues that block this issue")
|
|
2188
|
+
}
|
|
2189
|
+
},
|
|
2190
|
+
async ({ number, direction, repo }) => {
|
|
2191
|
+
console.error(`Getting dependencies for ${number} (direction: ${direction})${repo ? ` in ${repo}` : ""}`);
|
|
2192
|
+
try {
|
|
2193
|
+
const provider = IssueManagementProviderFactory.create(
|
|
2194
|
+
process.env.ISSUE_PROVIDER
|
|
2195
|
+
);
|
|
2196
|
+
const result = await provider.getDependencies({ number, direction, repo });
|
|
2197
|
+
console.error(`Dependencies fetched: ${result.blocking.length} blocking, ${result.blockedBy.length} blocked_by`);
|
|
2198
|
+
return {
|
|
2199
|
+
content: [
|
|
2200
|
+
{
|
|
2201
|
+
type: "text",
|
|
2202
|
+
text: JSON.stringify(result)
|
|
2203
|
+
}
|
|
2204
|
+
],
|
|
2205
|
+
structuredContent: result
|
|
2206
|
+
};
|
|
2207
|
+
} catch (error) {
|
|
2208
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
2209
|
+
console.error(`Failed to get dependencies: ${errorMessage}`);
|
|
2210
|
+
throw new Error(`Failed to get dependencies: ${errorMessage}`);
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
);
|
|
2214
|
+
server.registerTool(
|
|
2215
|
+
"remove_dependency",
|
|
2216
|
+
{
|
|
2217
|
+
title: "Remove Dependency",
|
|
2218
|
+
description: "Remove a blocking dependency between two issues. The blockingIssue will no longer block the blockedIssue.",
|
|
2219
|
+
inputSchema: {
|
|
2220
|
+
blockingIssue: z.string().describe('The issue that blocks (GitHub issue number or Linear identifier like "ENG-123")'),
|
|
2221
|
+
blockedIssue: z.string().describe('The issue being blocked (GitHub issue number or Linear identifier like "ENG-123")'),
|
|
2222
|
+
repo: z.string().optional().describe(
|
|
2223
|
+
'Optional repository in "owner/repo" format or full GitHub URL. When not provided, uses the current repository. GitHub only.'
|
|
2224
|
+
)
|
|
2225
|
+
},
|
|
2226
|
+
outputSchema: {
|
|
2227
|
+
success: z.boolean().describe("Whether the dependency was removed successfully")
|
|
2228
|
+
}
|
|
2229
|
+
},
|
|
2230
|
+
async ({ blockingIssue, blockedIssue, repo }) => {
|
|
2231
|
+
console.error(`Removing dependency: ${blockingIssue} blocks ${blockedIssue}${repo ? ` in ${repo}` : ""}`);
|
|
2232
|
+
try {
|
|
2233
|
+
const provider = IssueManagementProviderFactory.create(
|
|
2234
|
+
process.env.ISSUE_PROVIDER
|
|
2235
|
+
);
|
|
2236
|
+
await provider.removeDependency({ blockingIssue, blockedIssue, repo });
|
|
2237
|
+
console.error(`Dependency removed successfully: ${blockingIssue} -> ${blockedIssue}`);
|
|
2238
|
+
return {
|
|
2239
|
+
content: [
|
|
2240
|
+
{
|
|
2241
|
+
type: "text",
|
|
2242
|
+
text: JSON.stringify({ success: true })
|
|
2243
|
+
}
|
|
2244
|
+
],
|
|
2245
|
+
structuredContent: { success: true }
|
|
2246
|
+
};
|
|
2247
|
+
} catch (error) {
|
|
2248
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
2249
|
+
console.error(`Failed to remove dependency: ${errorMessage}`);
|
|
2250
|
+
throw new Error(`Failed to remove dependency: ${errorMessage}`);
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
);
|
|
2254
|
+
var childIssueResultSchema = z.object({
|
|
2255
|
+
id: z.string().describe("Issue identifier"),
|
|
2256
|
+
title: z.string().describe("Issue title"),
|
|
2257
|
+
url: z.string().describe("Issue URL"),
|
|
2258
|
+
state: z.string().describe("Issue state")
|
|
2259
|
+
});
|
|
2260
|
+
server.registerTool(
|
|
2261
|
+
"get_child_issues",
|
|
2262
|
+
{
|
|
2263
|
+
title: "Get Child Issues",
|
|
2264
|
+
description: "Get child issues (sub-issues) of a parent issue. Returns a list of issues that are children of the specified parent.",
|
|
2265
|
+
inputSchema: {
|
|
2266
|
+
number: z.string().describe('Parent issue identifier (GitHub issue number or Linear identifier like "ENG-123")'),
|
|
2267
|
+
repo: z.string().optional().describe(
|
|
2268
|
+
'Optional repository in "owner/repo" format or full GitHub URL. When not provided, uses the current repository. GitHub only.'
|
|
2269
|
+
)
|
|
2270
|
+
},
|
|
2271
|
+
outputSchema: {
|
|
2272
|
+
children: z.array(childIssueResultSchema).describe("Child issues of the parent")
|
|
2273
|
+
}
|
|
2274
|
+
},
|
|
2275
|
+
async ({ number, repo }) => {
|
|
2276
|
+
console.error(`Getting child issues for ${number}${repo ? ` in ${repo}` : ""}`);
|
|
2277
|
+
try {
|
|
2278
|
+
const provider = IssueManagementProviderFactory.create(
|
|
2279
|
+
process.env.ISSUE_PROVIDER
|
|
2280
|
+
);
|
|
2281
|
+
const result = await provider.getChildIssues({ number, repo });
|
|
2282
|
+
console.error(`Child issues fetched: ${result.length} children`);
|
|
2283
|
+
return {
|
|
2284
|
+
content: [
|
|
2285
|
+
{
|
|
2286
|
+
type: "text",
|
|
2287
|
+
text: JSON.stringify({ children: result })
|
|
2288
|
+
}
|
|
2289
|
+
],
|
|
2290
|
+
structuredContent: { children: result }
|
|
2291
|
+
};
|
|
2292
|
+
} catch (error) {
|
|
2293
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
2294
|
+
console.error(`Failed to get child issues: ${errorMessage}`);
|
|
2295
|
+
throw new Error(`Failed to get child issues: ${errorMessage}`);
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
);
|
|
1474
2299
|
async function main() {
|
|
1475
2300
|
console.error("Starting Issue Management MCP Server...");
|
|
1476
2301
|
const provider = validateEnvironment();
|