@iloom/cli 0.9.1 → 0.10.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.
Files changed (222) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +179 -41
  3. package/dist/{BranchNamingService-K6XNWQ6C.js → BranchNamingService-ECJHBB67.js} +2 -2
  4. package/dist/ClaudeContextManager-QXX6ZFST.js +14 -0
  5. package/dist/ClaudeService-NJNK2SUH.js +13 -0
  6. package/dist/{GitHubService-O7T6CFAJ.js → GitHubService-MEHKHUQP.js} +4 -4
  7. package/dist/IssueTrackerFactory-NG53YX5S.js +14 -0
  8. package/dist/{LoomLauncher-3I47SUPV.js → LoomLauncher-L64HHS3T.js} +9 -9
  9. package/dist/{MetadataManager-W3C54UYT.js → MetadataManager-5QZSTKNN.js} +2 -2
  10. package/dist/{ProjectCapabilityDetector-N5L7T4IY.js → ProjectCapabilityDetector-5KSYUTBJ.js} +3 -3
  11. package/dist/{PromptTemplateManager-36YLQRHP.js → PromptTemplateManager-DULSVRRE.js} +2 -2
  12. package/dist/README.md +179 -41
  13. package/dist/{SettingsManager-QR7V2IW2.js → SettingsManager-BQDQA3FK.js} +4 -2
  14. package/dist/agents/iloom-artifact-reviewer.md +11 -0
  15. package/dist/agents/iloom-code-reviewer.md +14 -0
  16. package/dist/agents/iloom-issue-analyze-and-plan.md +55 -12
  17. package/dist/agents/iloom-issue-analyzer.md +49 -6
  18. package/dist/agents/iloom-issue-complexity-evaluator.md +47 -6
  19. package/dist/agents/iloom-issue-enhancer.md +86 -7
  20. package/dist/agents/iloom-issue-implementer.md +48 -7
  21. package/dist/agents/iloom-issue-planner.md +115 -62
  22. package/dist/{build-IC4CJRMP.js → build-5GO3XW26.js} +9 -9
  23. package/dist/{chunk-USSL2X4A.js → chunk-3D7WQM7I.js} +2 -2
  24. package/dist/chunk-4232AHNQ.js +35 -0
  25. package/dist/chunk-4232AHNQ.js.map +1 -0
  26. package/dist/{chunk-QN47QVBX.js → chunk-4WJNIR5O.js} +1 -1
  27. package/dist/chunk-4WJNIR5O.js.map +1 -0
  28. package/dist/{chunk-2JPXGGP4.js → chunk-5MWV33NN.js} +4 -4
  29. package/dist/{chunk-POU2UMWN.js → chunk-6EU6TCF6.js} +10 -10
  30. package/dist/chunk-6EU6TCF6.js.map +1 -0
  31. package/dist/{chunk-Y5O2ALDZ.js → chunk-FB47TIJG.js} +29 -11
  32. package/dist/chunk-FB47TIJG.js.map +1 -0
  33. package/dist/chunk-HEXKPKCK.js +1396 -0
  34. package/dist/chunk-HEXKPKCK.js.map +1 -0
  35. package/dist/{chunk-KAYXR544.js → chunk-J5S7DFYC.js} +2 -2
  36. package/dist/{chunk-OK7LUTRW.js → chunk-JO2LZ6EQ.js} +476 -5
  37. package/dist/chunk-JO2LZ6EQ.js.map +1 -0
  38. package/dist/{chunk-KBEIQP4G.js → chunk-KB64WNBZ.js} +43 -3
  39. package/dist/chunk-KB64WNBZ.js.map +1 -0
  40. package/dist/{chunk-Y5HSSIK2.js → chunk-KXDRI47U.js} +71 -13
  41. package/dist/chunk-KXDRI47U.js.map +1 -0
  42. package/dist/{chunk-HZXBHMVM.js → chunk-LXLMMXXY.js} +54 -14
  43. package/dist/chunk-LXLMMXXY.js.map +1 -0
  44. package/dist/{chunk-H6ST2TGP.js → chunk-MNHZB4Z2.js} +4 -4
  45. package/dist/{chunk-TL72BGP6.js → chunk-MORRVYPT.js} +2 -2
  46. package/dist/{chunk-TGRK3CHF.js → chunk-NRSWLOAZ.js} +8 -8
  47. package/dist/chunk-NRSWLOAZ.js.map +1 -0
  48. package/dist/{chunk-FO5GGFOV.js → chunk-ONQYPICO.js} +13 -5
  49. package/dist/chunk-ONQYPICO.js.map +1 -0
  50. package/dist/{chunk-7ZEHSSUP.js → chunk-P4O6EH46.js} +4 -4
  51. package/dist/chunk-QZWEJVWV.js +207 -0
  52. package/dist/chunk-QZWEJVWV.js.map +1 -0
  53. package/dist/chunk-RSYT7MVI.js +202 -0
  54. package/dist/chunk-RSYT7MVI.js.map +1 -0
  55. package/dist/{chunk-OAVJR4PM.js → chunk-RYWFS37M.js} +6 -6
  56. package/dist/chunk-RYWFS37M.js.map +1 -0
  57. package/dist/{chunk-B7U6OKUR.js → chunk-SF2P22EE.js} +11 -3
  58. package/dist/chunk-SF2P22EE.js.map +1 -0
  59. package/dist/{chunk-MZPRBNYC.js → chunk-SN3SQCFK.js} +10 -8
  60. package/dist/{chunk-MZPRBNYC.js.map → chunk-SN3SQCFK.js.map} +1 -1
  61. package/dist/{chunk-4ZIHFUPN.js → chunk-UD3WJDIV.js} +145 -107
  62. package/dist/chunk-UD3WJDIV.js.map +1 -0
  63. package/dist/{chunk-3P6J4IZZ.js → chunk-UKBAJ2QQ.js} +61 -7
  64. package/dist/chunk-UKBAJ2QQ.js.map +1 -0
  65. package/dist/{chunk-RD7OPXZK.js → chunk-UVD4CZKS.js} +3 -3
  66. package/dist/chunk-UWGVCXRF.js +207 -0
  67. package/dist/chunk-UWGVCXRF.js.map +1 -0
  68. package/dist/{chunk-JT5LZRMI.js → chunk-VECNX6VX.js} +2 -2
  69. package/dist/{chunk-TRUMP4DA.js → chunk-VG45TUYK.js} +75 -6
  70. package/dist/chunk-VG45TUYK.js.map +1 -0
  71. package/dist/{chunk-4GAJJUYS.js → chunk-VGGST52X.js} +2 -2
  72. package/dist/{chunk-4LKGCFGG.js → chunk-WWKOVDWC.js} +2 -2
  73. package/dist/{chunk-2HZX6AMR.js → chunk-WY4QBK43.js} +7 -7
  74. package/dist/chunk-WY4QBK43.js.map +1 -0
  75. package/dist/chunk-Y4YZTHZE.js +73 -0
  76. package/dist/chunk-Y4YZTHZE.js.map +1 -0
  77. package/dist/{chunk-VOGGLPG5.js → chunk-YQ57ORTV.js} +14 -1
  78. package/dist/chunk-YQ57ORTV.js.map +1 -0
  79. package/dist/{chunk-XFEK2X2D.js → chunk-YYAKPQBT.js} +73 -20
  80. package/dist/chunk-YYAKPQBT.js.map +1 -0
  81. package/dist/{chunk-NTTSUAVM.js → chunk-ZEWU5PZK.js} +2 -2
  82. package/dist/{chunk-5LVVQGB3.js → chunk-ZHPNZC75.js} +17 -17
  83. package/dist/chunk-ZHPNZC75.js.map +1 -0
  84. package/dist/{chunk-I3HMNWQQ.js → chunk-ZW2LKWWE.js} +9 -9
  85. package/dist/chunk-ZW2LKWWE.js.map +1 -0
  86. package/dist/{claude-TP2QO3BU.js → claude-P3NQR6IJ.js} +2 -2
  87. package/dist/{cleanup-D3CSRBBZ.js → cleanup-6UCPVMFG.js} +81 -32
  88. package/dist/cleanup-6UCPVMFG.js.map +1 -0
  89. package/dist/cli.js +640 -350
  90. package/dist/cli.js.map +1 -1
  91. package/dist/{commit-IWGT42XN.js → commit-L3EPY5QG.js} +23 -21
  92. package/dist/commit-L3EPY5QG.js.map +1 -0
  93. package/dist/{compile-EOWJORKO.js → compile-ZS4HYRX5.js} +9 -9
  94. package/dist/{contribute-WSJTV2RX.js → contribute-ORDDQGSL.js} +14 -6
  95. package/dist/contribute-ORDDQGSL.js.map +1 -0
  96. package/dist/{dev-server-Q6M62ATG.js → dev-server-FYZ2AQIH.js} +29 -15
  97. package/dist/dev-server-FYZ2AQIH.js.map +1 -0
  98. package/dist/{feedback-QPNDZQRV.js → feedback-TMBXSCM5.js} +15 -15
  99. package/dist/{git-W3XUIFTR.js → git-ET64COO3.js} +4 -4
  100. package/dist/hooks/iloom-hook.js +15 -0
  101. package/dist/ignite-CGOV3TD4.js +1393 -0
  102. package/dist/ignite-CGOV3TD4.js.map +1 -0
  103. package/dist/index.d.ts +397 -53
  104. package/dist/index.js +1178 -40
  105. package/dist/index.js.map +1 -1
  106. package/dist/{init-ALYWKNWG.js → init-GFQ5W7GK.js} +57 -21
  107. package/dist/init-GFQ5W7GK.js.map +1 -0
  108. package/dist/issues-T4ZZSPEG.js +179 -0
  109. package/dist/issues-T4ZZSPEG.js.map +1 -0
  110. package/dist/{lint-IHUH45OC.js → lint-6TQXDZ3T.js} +9 -9
  111. package/dist/mcp/issue-management-server.js +2472 -257
  112. package/dist/mcp/issue-management-server.js.map +1 -1
  113. package/dist/mcp/recap-server.js +144 -21
  114. package/dist/mcp/recap-server.js.map +1 -1
  115. package/dist/{neon-helpers-VVFFTLXE.js → neon-helpers-CQN2PB4S.js} +3 -3
  116. package/dist/neon-helpers-CQN2PB4S.js.map +1 -0
  117. package/dist/{open-KWOV2OFO.js → open-5QZGXQRF.js} +15 -15
  118. package/dist/open-5QZGXQRF.js.map +1 -0
  119. package/dist/{plan-BRJBFJHF.js → plan-U7ZQWLFY.js} +41 -25
  120. package/dist/plan-U7ZQWLFY.js.map +1 -0
  121. package/dist/{projects-LH362JZQ.js → projects-2UOXFLNZ.js} +4 -4
  122. package/dist/prompts/CLAUDE.md +62 -0
  123. package/dist/prompts/init-prompt.txt +386 -47
  124. package/dist/prompts/issue-prompt.txt +427 -54
  125. package/dist/prompts/plan-prompt.txt +97 -16
  126. package/dist/prompts/pr-prompt.txt +44 -1
  127. package/dist/prompts/regular-prompt.txt +42 -1
  128. package/dist/prompts/session-summary-prompt.txt +14 -0
  129. package/dist/prompts/swarm-orchestrator-prompt.txt +437 -0
  130. package/dist/{rebase-AJOJOZUG.js → rebase-DWIB77KV.js} +10 -10
  131. package/dist/{recap-GKJXMDXW.js → recap-MX63HAKV.js} +47 -19
  132. package/dist/recap-MX63HAKV.js.map +1 -0
  133. package/dist/{run-QEUVZF7J.js → run-O3TFNQFC.js} +15 -15
  134. package/dist/run-O3TFNQFC.js.map +1 -0
  135. package/dist/schema/package-iloom.schema.json +58 -0
  136. package/dist/schema/settings.schema.json +130 -15
  137. package/dist/{shell-DAAVG4YN.js → shell-G6VC2CYR.js} +14 -7
  138. package/dist/shell-G6VC2CYR.js.map +1 -0
  139. package/dist/{summary-ZKOA35PT.js → summary-FWHAX55O.js} +27 -25
  140. package/dist/summary-FWHAX55O.js.map +1 -0
  141. package/dist/{test-5GPWWO3P.js → test-F7JNJZYP.js} +9 -9
  142. package/dist/{test-git-EJUKDB7F.js → test-git-BTAOIUE2.js} +4 -4
  143. package/dist/test-jira-CHYNV33F.js +96 -0
  144. package/dist/test-jira-CHYNV33F.js.map +1 -0
  145. package/dist/{test-prefix-23TOBUXY.js → test-prefix-Q6TFSU6F.js} +4 -4
  146. package/dist/{test-webserver-CKROHFBQ.js → test-webserver-EONCG7E7.js} +6 -6
  147. package/dist/{vscode-6TOLFCI2.js → vscode-VA5X4P25.js} +7 -7
  148. package/package.json +5 -1
  149. package/dist/ClaudeContextManager-X2Y72GRL.js +0 -14
  150. package/dist/ClaudeService-7P32TTES.js +0 -13
  151. package/dist/chunk-2HZX6AMR.js.map +0 -1
  152. package/dist/chunk-3P6J4IZZ.js.map +0 -1
  153. package/dist/chunk-4ZIHFUPN.js.map +0 -1
  154. package/dist/chunk-5LVVQGB3.js.map +0 -1
  155. package/dist/chunk-B7U6OKUR.js.map +0 -1
  156. package/dist/chunk-ENGCJIYQ.js +0 -520
  157. package/dist/chunk-ENGCJIYQ.js.map +0 -1
  158. package/dist/chunk-FO5GGFOV.js.map +0 -1
  159. package/dist/chunk-HZXBHMVM.js.map +0 -1
  160. package/dist/chunk-I3HMNWQQ.js.map +0 -1
  161. package/dist/chunk-J7FJ6PUT.js +0 -121
  162. package/dist/chunk-J7FJ6PUT.js.map +0 -1
  163. package/dist/chunk-KBEIQP4G.js.map +0 -1
  164. package/dist/chunk-OAVJR4PM.js.map +0 -1
  165. package/dist/chunk-OK7LUTRW.js.map +0 -1
  166. package/dist/chunk-POU2UMWN.js.map +0 -1
  167. package/dist/chunk-QN47QVBX.js.map +0 -1
  168. package/dist/chunk-TGRK3CHF.js.map +0 -1
  169. package/dist/chunk-TRUMP4DA.js.map +0 -1
  170. package/dist/chunk-VOGGLPG5.js.map +0 -1
  171. package/dist/chunk-XFEK2X2D.js.map +0 -1
  172. package/dist/chunk-Y5HSSIK2.js.map +0 -1
  173. package/dist/chunk-Y5O2ALDZ.js.map +0 -1
  174. package/dist/cleanup-D3CSRBBZ.js.map +0 -1
  175. package/dist/commit-IWGT42XN.js.map +0 -1
  176. package/dist/contribute-WSJTV2RX.js.map +0 -1
  177. package/dist/dev-server-Q6M62ATG.js.map +0 -1
  178. package/dist/ignite-OPO6EDYT.js +0 -784
  179. package/dist/ignite-OPO6EDYT.js.map +0 -1
  180. package/dist/init-ALYWKNWG.js.map +0 -1
  181. package/dist/issues-L7TBUPXT.js +0 -116
  182. package/dist/issues-L7TBUPXT.js.map +0 -1
  183. package/dist/open-KWOV2OFO.js.map +0 -1
  184. package/dist/plan-BRJBFJHF.js.map +0 -1
  185. package/dist/recap-GKJXMDXW.js.map +0 -1
  186. package/dist/run-QEUVZF7J.js.map +0 -1
  187. package/dist/shell-DAAVG4YN.js.map +0 -1
  188. package/dist/summary-ZKOA35PT.js.map +0 -1
  189. /package/dist/{BranchNamingService-K6XNWQ6C.js.map → BranchNamingService-ECJHBB67.js.map} +0 -0
  190. /package/dist/{ClaudeContextManager-X2Y72GRL.js.map → ClaudeContextManager-QXX6ZFST.js.map} +0 -0
  191. /package/dist/{ClaudeService-7P32TTES.js.map → ClaudeService-NJNK2SUH.js.map} +0 -0
  192. /package/dist/{GitHubService-O7T6CFAJ.js.map → GitHubService-MEHKHUQP.js.map} +0 -0
  193. /package/dist/{MetadataManager-W3C54UYT.js.map → IssueTrackerFactory-NG53YX5S.js.map} +0 -0
  194. /package/dist/{LoomLauncher-3I47SUPV.js.map → LoomLauncher-L64HHS3T.js.map} +0 -0
  195. /package/dist/{ProjectCapabilityDetector-N5L7T4IY.js.map → MetadataManager-5QZSTKNN.js.map} +0 -0
  196. /package/dist/{PromptTemplateManager-36YLQRHP.js.map → ProjectCapabilityDetector-5KSYUTBJ.js.map} +0 -0
  197. /package/dist/{SettingsManager-QR7V2IW2.js.map → PromptTemplateManager-DULSVRRE.js.map} +0 -0
  198. /package/dist/{claude-TP2QO3BU.js.map → SettingsManager-BQDQA3FK.js.map} +0 -0
  199. /package/dist/{build-IC4CJRMP.js.map → build-5GO3XW26.js.map} +0 -0
  200. /package/dist/{chunk-USSL2X4A.js.map → chunk-3D7WQM7I.js.map} +0 -0
  201. /package/dist/{chunk-2JPXGGP4.js.map → chunk-5MWV33NN.js.map} +0 -0
  202. /package/dist/{chunk-KAYXR544.js.map → chunk-J5S7DFYC.js.map} +0 -0
  203. /package/dist/{chunk-H6ST2TGP.js.map → chunk-MNHZB4Z2.js.map} +0 -0
  204. /package/dist/{chunk-TL72BGP6.js.map → chunk-MORRVYPT.js.map} +0 -0
  205. /package/dist/{chunk-7ZEHSSUP.js.map → chunk-P4O6EH46.js.map} +0 -0
  206. /package/dist/{chunk-RD7OPXZK.js.map → chunk-UVD4CZKS.js.map} +0 -0
  207. /package/dist/{chunk-JT5LZRMI.js.map → chunk-VECNX6VX.js.map} +0 -0
  208. /package/dist/{chunk-4GAJJUYS.js.map → chunk-VGGST52X.js.map} +0 -0
  209. /package/dist/{chunk-4LKGCFGG.js.map → chunk-WWKOVDWC.js.map} +0 -0
  210. /package/dist/{chunk-NTTSUAVM.js.map → chunk-ZEWU5PZK.js.map} +0 -0
  211. /package/dist/{git-W3XUIFTR.js.map → claude-P3NQR6IJ.js.map} +0 -0
  212. /package/dist/{compile-EOWJORKO.js.map → compile-ZS4HYRX5.js.map} +0 -0
  213. /package/dist/{feedback-QPNDZQRV.js.map → feedback-TMBXSCM5.js.map} +0 -0
  214. /package/dist/{neon-helpers-VVFFTLXE.js.map → git-ET64COO3.js.map} +0 -0
  215. /package/dist/{lint-IHUH45OC.js.map → lint-6TQXDZ3T.js.map} +0 -0
  216. /package/dist/{projects-LH362JZQ.js.map → projects-2UOXFLNZ.js.map} +0 -0
  217. /package/dist/{rebase-AJOJOZUG.js.map → rebase-DWIB77KV.js.map} +0 -0
  218. /package/dist/{test-5GPWWO3P.js.map → test-F7JNJZYP.js.map} +0 -0
  219. /package/dist/{test-git-EJUKDB7F.js.map → test-git-BTAOIUE2.js.map} +0 -0
  220. /package/dist/{test-prefix-23TOBUXY.js.map → test-prefix-Q6TFSU6F.js.map} +0 -0
  221. /package/dist/{test-webserver-CKROHFBQ.js.map → test-webserver-EONCG7E7.js.map} +0 -0
  222. /package/dist/{vscode-6TOLFCI2.js.map → vscode-VA5X4P25.js.map} +0 -0
@@ -3,7 +3,7 @@
3
3
  // src/mcp/issue-management-server.ts
4
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
- import { z } from "zod";
6
+ import { z as z2 } from "zod";
7
7
 
8
8
  // src/utils/github.ts
9
9
  import { execa as execa2 } from "execa";
@@ -377,6 +377,41 @@ async function removeIssueDependency(blockedIssueNumber, blockingIssueDatabaseId
377
377
  apiPath
378
378
  ]);
379
379
  }
380
+ async function closeGhIssue(issueNumber, repo) {
381
+ logger.debug("Closing GitHub issue", { issueNumber, repo });
382
+ const args = ["issue", "close", String(issueNumber)];
383
+ if (repo) {
384
+ args.push("--repo", repo);
385
+ }
386
+ await executeGhCommand(args);
387
+ }
388
+ async function reopenGhIssue(issueNumber, repo) {
389
+ logger.debug("Reopening GitHub issue", { issueNumber, repo });
390
+ const args = ["issue", "reopen", String(issueNumber)];
391
+ if (repo) {
392
+ args.push("--repo", repo);
393
+ }
394
+ await executeGhCommand(args);
395
+ }
396
+ async function editGhIssue(issueNumber, options, repo) {
397
+ logger.debug("Editing GitHub issue", { issueNumber, options, repo });
398
+ const args = ["issue", "edit", String(issueNumber)];
399
+ if (options.title !== void 0) {
400
+ args.push("--title", options.title);
401
+ }
402
+ if (options.body !== void 0) {
403
+ args.push("--body", options.body);
404
+ }
405
+ if (options.labels) {
406
+ if (options.labels.length > 0) {
407
+ args.push("--add-label", options.labels.join(","));
408
+ }
409
+ }
410
+ if (repo) {
411
+ args.push("--repo", repo);
412
+ }
413
+ await executeGhCommand(args);
414
+ }
380
415
 
381
416
  // src/utils/image-processor.ts
382
417
  import { tmpdir } from "os";
@@ -776,6 +811,55 @@ var GitHubIssueManagementProvider = class {
776
811
  }
777
812
  return result;
778
813
  }
814
+ /**
815
+ * Fetch PR review comments (inline code comments on specific files/lines)
816
+ * Uses gh api with --paginate to handle PRs with many review comments
817
+ * Optionally filters by review ID
818
+ */
819
+ async getReviewComments(input) {
820
+ const { number, reviewId, repo } = input;
821
+ const prNumber = parseInt(number, 10);
822
+ if (isNaN(prNumber)) {
823
+ throw new Error(`Invalid GitHub PR number: ${number}. GitHub PR IDs must be numeric.`);
824
+ }
825
+ let numericReviewId;
826
+ if (reviewId) {
827
+ numericReviewId = parseInt(reviewId, 10);
828
+ if (isNaN(numericReviewId)) {
829
+ throw new Error(`Invalid review ID: ${reviewId}. Review IDs must be numeric.`);
830
+ }
831
+ }
832
+ const apiPath = repo ? `repos/${repo}/pulls/${prNumber}/comments` : `repos/:owner/:repo/pulls/${prNumber}/comments`;
833
+ const args = [
834
+ "api",
835
+ apiPath,
836
+ "--paginate",
837
+ "--jq",
838
+ "[.[] | {id: .id, body: .body, path: .path, line: .line, side: .side, user: .user, created_at: .created_at, updated_at: .updated_at, in_reply_to_id: .in_reply_to_id, pull_request_review_id: .pull_request_review_id}]"
839
+ ];
840
+ const raw = await executeGhCommand(args);
841
+ let comments = raw;
842
+ if (numericReviewId !== void 0) {
843
+ comments = comments.filter((c) => c.pull_request_review_id === numericReviewId);
844
+ }
845
+ const results = [];
846
+ for (const comment of comments) {
847
+ const processedBody = await processMarkdownImages(comment.body, "github");
848
+ results.push({
849
+ id: String(comment.id),
850
+ body: processedBody,
851
+ path: comment.path,
852
+ line: comment.line,
853
+ side: comment.side,
854
+ author: normalizeAuthor(comment.user),
855
+ createdAt: comment.created_at,
856
+ updatedAt: comment.updated_at ?? null,
857
+ inReplyToId: comment.in_reply_to_id ? String(comment.in_reply_to_id) : null,
858
+ pullRequestReviewId: comment.pull_request_review_id
859
+ });
860
+ }
861
+ return results;
862
+ }
779
863
  /**
780
864
  * Fetch a specific comment by ID using gh API
781
865
  * Normalizes author to FlexibleAuthor format
@@ -935,6 +1019,55 @@ var GitHubIssueManagementProvider = class {
935
1019
  }
936
1020
  return await getSubIssues(issueNumber, repo);
937
1021
  }
1022
+ /**
1023
+ * Close an issue
1024
+ */
1025
+ async closeIssue(input) {
1026
+ const { number, repo } = input;
1027
+ const issueNumber = parseInt(number, 10);
1028
+ if (isNaN(issueNumber)) {
1029
+ throw new Error(`Invalid GitHub issue number: ${number}. GitHub issue IDs must be numeric.`);
1030
+ }
1031
+ await closeGhIssue(issueNumber, repo);
1032
+ }
1033
+ /**
1034
+ * Reopen a closed issue
1035
+ */
1036
+ async reopenIssue(input) {
1037
+ const { number, repo } = input;
1038
+ const issueNumber = parseInt(number, 10);
1039
+ if (isNaN(issueNumber)) {
1040
+ throw new Error(`Invalid GitHub issue number: ${number}. GitHub issue IDs must be numeric.`);
1041
+ }
1042
+ await reopenGhIssue(issueNumber, repo);
1043
+ }
1044
+ /**
1045
+ * Edit an issue's properties
1046
+ * State changes are delegated to closeIssue/reopenIssue
1047
+ */
1048
+ async editIssue(input) {
1049
+ const { number, title, body, state, labels, repo } = input;
1050
+ const issueNumber = parseInt(number, 10);
1051
+ if (isNaN(issueNumber)) {
1052
+ throw new Error(`Invalid GitHub issue number: ${number}. GitHub issue IDs must be numeric.`);
1053
+ }
1054
+ if (state === "closed") {
1055
+ await this.closeIssue({ number, repo });
1056
+ } else if (state === "open") {
1057
+ await this.reopenIssue({ number, repo });
1058
+ }
1059
+ if (title !== void 0 || body !== void 0 || labels !== void 0) {
1060
+ await editGhIssue(
1061
+ issueNumber,
1062
+ {
1063
+ ...title !== void 0 && { title },
1064
+ ...body !== void 0 && { body },
1065
+ ...labels !== void 0 && { labels }
1066
+ },
1067
+ repo
1068
+ );
1069
+ }
1070
+ }
938
1071
  };
939
1072
 
940
1073
  // src/utils/linear.ts
@@ -1141,6 +1274,61 @@ async function createLinearComment(identifier, body) {
1141
1274
  handleLinearError(error, "createLinearComment");
1142
1275
  }
1143
1276
  }
1277
+ async function updateLinearIssueState(identifier, stateName) {
1278
+ try {
1279
+ logger.debug(`Updating Linear issue ${identifier} state to: ${stateName}`);
1280
+ const client = createLinearClient();
1281
+ const issue = await client.issue(identifier);
1282
+ if (!issue) {
1283
+ throw new LinearServiceError("NOT_FOUND", `Linear issue ${identifier} not found`);
1284
+ }
1285
+ const team = await issue.team;
1286
+ if (!team) {
1287
+ throw new LinearServiceError("CLI_ERROR", "Issue has no team");
1288
+ }
1289
+ const states = await team.states();
1290
+ const state = states.nodes.find((s) => s.name === stateName);
1291
+ if (!state) {
1292
+ throw new LinearServiceError(
1293
+ "NOT_FOUND",
1294
+ `State "${stateName}" not found in team ${team.key}`
1295
+ );
1296
+ }
1297
+ await client.updateIssue(issue.id, {
1298
+ stateId: state.id
1299
+ });
1300
+ } catch (error) {
1301
+ if (error instanceof LinearServiceError) {
1302
+ throw error;
1303
+ }
1304
+ handleLinearError(error, "updateLinearIssueState");
1305
+ }
1306
+ }
1307
+ async function editLinearIssue(identifier, updates) {
1308
+ try {
1309
+ logger.debug(`Editing Linear issue ${identifier}`, { updates });
1310
+ const client = createLinearClient();
1311
+ const issue = await client.issue(identifier);
1312
+ if (!issue) {
1313
+ throw new LinearServiceError("NOT_FOUND", `Linear issue ${identifier} not found`);
1314
+ }
1315
+ const updatePayload = {};
1316
+ if (updates.title !== void 0) {
1317
+ updatePayload.title = updates.title;
1318
+ }
1319
+ if (updates.description !== void 0) {
1320
+ updatePayload.description = updates.description;
1321
+ }
1322
+ if (Object.keys(updatePayload).length > 0) {
1323
+ await client.updateIssue(issue.id, updatePayload);
1324
+ }
1325
+ } catch (error) {
1326
+ if (error instanceof LinearServiceError) {
1327
+ throw error;
1328
+ }
1329
+ handleLinearError(error, "editLinearIssue");
1330
+ }
1331
+ }
1144
1332
  async function getLinearComment(commentId) {
1145
1333
  try {
1146
1334
  logger.debug(`Fetching Linear comment: ${commentId}`);
@@ -1209,10 +1397,10 @@ async function fetchLinearIssueComments(identifier) {
1209
1397
  handleLinearError(error, "fetchLinearIssueComments");
1210
1398
  }
1211
1399
  }
1212
- async function getLinearChildIssues(identifier) {
1400
+ async function getLinearChildIssues(identifier, options) {
1213
1401
  try {
1214
1402
  logger.debug(`Fetching child issues for Linear issue: ${identifier}`);
1215
- const client = createLinearClient();
1403
+ const client = createLinearClient(options == null ? void 0 : options.apiToken);
1216
1404
  const issue = await client.issue(identifier);
1217
1405
  if (!issue) {
1218
1406
  throw new LinearServiceError("NOT_FOUND", `Linear issue ${identifier} not found`);
@@ -1711,229 +1899,2098 @@ var LinearIssueManagementProvider = class {
1711
1899
  const { number } = input;
1712
1900
  return await getLinearChildIssues(number);
1713
1901
  }
1714
- };
1715
-
1716
- // src/mcp/IssueManagementProviderFactory.ts
1717
- var IssueManagementProviderFactory = class {
1718
1902
  /**
1719
- * Create an issue management provider based on the provider type
1903
+ * Close an issue by transitioning to "Done" state
1720
1904
  */
1721
- static create(provider) {
1722
- switch (provider) {
1723
- case "github":
1724
- return new GitHubIssueManagementProvider();
1725
- case "linear":
1726
- return new LinearIssueManagementProvider();
1727
- default:
1728
- throw new Error(`Unsupported issue management provider: ${provider}`);
1905
+ async closeIssue(input) {
1906
+ const { number } = input;
1907
+ await updateLinearIssueState(number, "Done");
1908
+ }
1909
+ /**
1910
+ * Reopen a closed issue by transitioning to "Todo" state
1911
+ */
1912
+ async reopenIssue(input) {
1913
+ const { number } = input;
1914
+ await updateLinearIssueState(number, "Todo");
1915
+ }
1916
+ /**
1917
+ * Edit an issue's properties
1918
+ * State changes are delegated to closeIssue/reopenIssue
1919
+ */
1920
+ async editIssue(input) {
1921
+ const { number, title, body, state } = input;
1922
+ if (state === "closed") {
1923
+ await this.closeIssue({ number });
1924
+ } else if (state === "open") {
1925
+ await this.reopenIssue({ number });
1926
+ }
1927
+ if (title !== void 0 || body !== void 0) {
1928
+ await editLinearIssue(number, {
1929
+ ...title !== void 0 && { title },
1930
+ ...body !== void 0 && { description: body }
1931
+ });
1729
1932
  }
1730
1933
  }
1731
1934
  };
1732
1935
 
1733
- // src/mcp/issue-management-server.ts
1734
- function validateEnvironment() {
1735
- const provider = process.env.ISSUE_PROVIDER;
1736
- if (!provider) {
1737
- console.error("Missing required environment variable: ISSUE_PROVIDER");
1738
- process.exit(1);
1936
+ // src/utils/jira.ts
1937
+ function escapeJql(value) {
1938
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
1939
+ }
1940
+
1941
+ // src/lib/providers/jira/JiraApiClient.ts
1942
+ import https from "https";
1943
+
1944
+ // src/utils/logger-context.ts
1945
+ import { AsyncLocalStorage } from "async_hooks";
1946
+ var loggerStorage = new AsyncLocalStorage();
1947
+ function getLogger() {
1948
+ return loggerStorage.getStore() ?? logger;
1949
+ }
1950
+
1951
+ // src/lib/providers/jira/AdfMarkdownConverter.ts
1952
+ import { Parser } from "extended-markdown-adf-parser";
1953
+ var parser = new Parser();
1954
+ function sanitizeCodeMarks(node) {
1955
+ var _a;
1956
+ if ((_a = node.marks) == null ? void 0 : _a.some((mark) => mark.type === "code")) {
1957
+ node.marks = [{ type: "code" }];
1739
1958
  }
1740
- if (provider !== "github" && provider !== "linear") {
1741
- console.error(`Invalid ISSUE_PROVIDER: ${provider}. Must be 'github' or 'linear'`);
1742
- process.exit(1);
1959
+ if (node.content && Array.isArray(node.content)) {
1960
+ node.content = node.content.map((child) => sanitizeCodeMarks(child));
1743
1961
  }
1744
- if (provider === "github") {
1745
- const required = ["REPO_OWNER", "REPO_NAME"];
1746
- const missing = required.filter((key) => !process.env[key]);
1747
- if (missing.length > 0) {
1748
- console.error(
1749
- `Missing required environment variables for GitHub provider: ${missing.join(", ")}`
1750
- );
1751
- process.exit(1);
1752
- }
1962
+ return node;
1963
+ }
1964
+ var BLOCK_LEVEL_TYPES = /* @__PURE__ */ new Set([
1965
+ "paragraph",
1966
+ "bulletList",
1967
+ "orderedList",
1968
+ "codeBlock",
1969
+ "heading",
1970
+ "blockquote",
1971
+ "rule",
1972
+ "mediaGroup",
1973
+ "nestedExpand",
1974
+ "panel",
1975
+ "table",
1976
+ "taskList",
1977
+ "decisionList",
1978
+ "mediaSingle"
1979
+ ]);
1980
+ function wrapTableCellContent(node) {
1981
+ if (node.content && Array.isArray(node.content)) {
1982
+ node.content = node.content.map((child) => wrapTableCellContent(child));
1753
1983
  }
1754
- if (provider === "linear") {
1755
- if (!process.env.LINEAR_API_TOKEN) {
1756
- console.error("Missing required environment variable for Linear provider: LINEAR_API_TOKEN");
1757
- process.exit(1);
1758
- }
1984
+ if (node.type !== "tableCell" && node.type !== "tableHeader") {
1985
+ return node;
1759
1986
  }
1760
- return provider;
1761
- }
1762
- var server = new McpServer({
1763
- name: "issue-management-broker",
1764
- version: "0.1.0"
1765
- });
1766
- var flexibleAuthorSchema = z.object({
1767
- id: z.string(),
1768
- displayName: z.string()
1769
- }).passthrough();
1770
- server.registerTool(
1771
- "get_issue",
1772
- {
1773
- title: "Get Issue",
1774
- description: "Fetch issue details including body, title, comments, labels, assignees, and other metadata. Author fields vary by provider: GitHub uses { login }, Linear uses { name, displayName }, Jira uses { displayName, accountId }. All authors have normalized core fields: { id, displayName } plus provider-specific fields.",
1775
- inputSchema: {
1776
- number: z.string().describe("The issue identifier"),
1777
- includeComments: z.boolean().optional().describe("Whether to include comments (default: true)"),
1778
- repo: z.string().optional().describe(
1779
- 'Optional repository in "owner/repo" format or full GitHub URL. When not provided, uses the current repository. GitHub only.'
1780
- )
1781
- },
1782
- outputSchema: {
1783
- // Core validated fields
1784
- id: z.string().describe("Issue identifier"),
1785
- title: z.string().describe("Issue title"),
1786
- body: z.string().describe("Issue body/description"),
1787
- state: z.string().describe("Issue state (open, closed, etc.)"),
1788
- url: z.string().describe("Issue URL"),
1789
- provider: z.enum(["github", "linear"]).describe("Issue management provider"),
1790
- // Flexible author - core fields + passthrough
1791
- author: flexibleAuthorSchema.nullable().describe(
1792
- "Issue author with normalized { id, displayName } plus provider-specific fields"
1793
- ),
1794
- // Optional flexible arrays
1795
- assignees: z.array(flexibleAuthorSchema).optional().describe(
1796
- "Issue assignees with normalized { id, displayName } plus provider-specific fields"
1797
- ),
1798
- labels: z.array(
1799
- z.object({ name: z.string() }).passthrough()
1800
- ).optional().describe("Issue labels"),
1801
- // Comments with flexible author
1802
- comments: z.array(
1803
- z.object({
1804
- id: z.string(),
1805
- body: z.string(),
1806
- author: flexibleAuthorSchema.nullable(),
1807
- createdAt: z.string()
1808
- }).passthrough()
1809
- ).optional().describe("Issue comments with flexible author structure")
1987
+ if (!node.content || node.content.length === 0) {
1988
+ return node;
1989
+ }
1990
+ const allInline = node.content.every((child) => !BLOCK_LEVEL_TYPES.has(child.type));
1991
+ if (allInline) {
1992
+ node.content = [{ type: "paragraph", content: node.content }];
1993
+ } else {
1994
+ const newContent = [];
1995
+ let inlineRun = [];
1996
+ for (const child of node.content) {
1997
+ if (BLOCK_LEVEL_TYPES.has(child.type)) {
1998
+ if (inlineRun.length > 0) {
1999
+ newContent.push({ type: "paragraph", content: inlineRun });
2000
+ inlineRun = [];
2001
+ }
2002
+ newContent.push(child);
2003
+ } else {
2004
+ inlineRun.push(child);
2005
+ }
1810
2006
  }
1811
- },
1812
- async ({ number, includeComments, repo }) => {
1813
- console.error(`Fetching issue ${number}${repo ? ` from ${repo}` : ""}`);
1814
- try {
1815
- const provider = IssueManagementProviderFactory.create(
1816
- process.env.ISSUE_PROVIDER
1817
- );
1818
- const result = await provider.getIssue({ number, includeComments, repo });
1819
- console.error(`Issue fetched successfully: ${result.number} - ${result.title}`);
1820
- return {
1821
- content: [
1822
- {
1823
- type: "text",
1824
- text: JSON.stringify(result)
1825
- }
1826
- ],
1827
- structuredContent: result
1828
- };
1829
- } catch (error) {
1830
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
1831
- console.error(`Failed to fetch issue: ${errorMessage}`);
1832
- throw new Error(`Failed to fetch issue: ${errorMessage}`);
2007
+ if (inlineRun.length > 0) {
2008
+ newContent.push({ type: "paragraph", content: inlineRun });
1833
2009
  }
2010
+ node.content = newContent;
1834
2011
  }
1835
- );
1836
- server.registerTool(
1837
- "get_pr",
1838
- {
1839
- title: "Get Pull Request",
1840
- description: "Fetch pull request details including title, body, comments, files, commits, and branch information. PRs only exist on GitHub, so this tool always uses GitHub regardless of configured issue tracker. Author fields have normalized core fields: { id, displayName } plus provider-specific fields.",
1841
- inputSchema: {
1842
- number: z.string().describe("The PR number"),
1843
- includeComments: z.boolean().optional().describe("Whether to include comments (default: true)"),
1844
- repo: z.string().optional().describe(
1845
- 'Optional repository in "owner/repo" format or full GitHub URL. When not provided, uses the current repository.'
1846
- )
1847
- },
1848
- outputSchema: {
1849
- // Core validated fields
1850
- id: z.string().describe("PR identifier"),
1851
- number: z.number().describe("PR number"),
1852
- title: z.string().describe("PR title"),
1853
- body: z.string().describe("PR body/description"),
1854
- state: z.string().describe("PR state (OPEN, CLOSED, MERGED)"),
1855
- url: z.string().describe("PR URL"),
1856
- // Branch info
1857
- headRefName: z.string().describe("Source branch name"),
1858
- baseRefName: z.string().describe("Target branch name"),
1859
- // Flexible author - core fields + passthrough
1860
- author: flexibleAuthorSchema.nullable().describe(
1861
- "PR author with normalized { id, displayName } plus provider-specific fields"
1862
- ),
1863
- // Optional flexible arrays
1864
- files: z.array(
1865
- z.object({
1866
- path: z.string(),
1867
- additions: z.number(),
1868
- deletions: z.number()
1869
- }).passthrough()
1870
- ).optional().describe("Changed files in the PR"),
1871
- commits: z.array(
1872
- z.object({
1873
- oid: z.string(),
1874
- messageHeadline: z.string(),
1875
- author: flexibleAuthorSchema.nullable()
1876
- }).passthrough()
1877
- ).optional().describe("Commits in the PR"),
1878
- comments: z.array(
1879
- z.object({
1880
- id: z.string(),
1881
- body: z.string(),
1882
- author: flexibleAuthorSchema.nullable(),
1883
- createdAt: z.string()
1884
- }).passthrough()
1885
- ).optional().describe("PR comments")
2012
+ return node;
2013
+ }
2014
+ var taskIdCounter = 0;
2015
+ function getCanonicalPlainText(text) {
2016
+ const miniAdf = parser.markdownToAdf(text);
2017
+ return getPlainText(miniAdf);
2018
+ }
2019
+ function extractCheckboxBlocks(markdown) {
2020
+ var _a, _b;
2021
+ const lines = markdown.split("\n");
2022
+ const blocks = [];
2023
+ let i = 0;
2024
+ while (i < lines.length) {
2025
+ const bulletLines = [];
2026
+ let blockIndent = null;
2027
+ while (i < lines.length) {
2028
+ const line = lines[i] ?? "";
2029
+ const checkboxMatch = line.match(/^(\s*)[-*+] \[([ xX])\] (.*)$/);
2030
+ if (checkboxMatch) {
2031
+ const indent = ((_a = checkboxMatch[1]) == null ? void 0 : _a.length) ?? 0;
2032
+ if (blockIndent === null) {
2033
+ blockIndent = indent;
2034
+ } else if (indent !== blockIndent) {
2035
+ break;
2036
+ }
2037
+ const state = checkboxMatch[2] === " " ? "TODO" : "DONE";
2038
+ bulletLines.push({ isCheckbox: true, state, rawText: checkboxMatch[3] ?? "" });
2039
+ i++;
2040
+ } else if (line.match(/^\s*[-*+] /)) {
2041
+ const indentMatch = line.match(/^(\s*)/);
2042
+ const indent = ((_b = indentMatch == null ? void 0 : indentMatch[1]) == null ? void 0 : _b.length) ?? 0;
2043
+ if (blockIndent === null) {
2044
+ blockIndent = indent;
2045
+ } else if (indent !== blockIndent) {
2046
+ break;
2047
+ }
2048
+ bulletLines.push({ isCheckbox: false, state: null, rawText: "" });
2049
+ i++;
2050
+ } else if (bulletLines.length > 0 && line.match(/^\s/) && line.trim() !== "") {
2051
+ const lastItem = bulletLines[bulletLines.length - 1];
2052
+ if (lastItem) {
2053
+ lastItem.rawText += "\n" + line.trim();
2054
+ }
2055
+ i++;
2056
+ } else {
2057
+ break;
2058
+ }
1886
2059
  }
1887
- },
1888
- async ({ number, includeComments, repo }) => {
1889
- console.error(`Fetching PR ${number}${repo ? ` from ${repo}` : ""}`);
1890
- try {
1891
- const provider = new GitHubIssueManagementProvider();
1892
- const result = await provider.getPR({ number, includeComments, repo });
1893
- console.error(`PR fetched successfully: #${result.number} - ${result.title}`);
1894
- return {
1895
- content: [
1896
- {
1897
- type: "text",
1898
- text: JSON.stringify(result)
1899
- }
1900
- ],
1901
- structuredContent: result
1902
- };
1903
- } catch (error) {
1904
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
1905
- console.error(`Failed to fetch PR: ${errorMessage}`);
1906
- throw new Error(`Failed to fetch PR: ${errorMessage}`);
2060
+ if (bulletLines.length > 0) {
2061
+ const allCheckboxes = bulletLines.every((l) => l.isCheckbox);
2062
+ if (allCheckboxes) {
2063
+ blocks.push({
2064
+ states: bulletLines.map((l) => l.state),
2065
+ texts: bulletLines.map((l) => getCanonicalPlainText(l.rawText))
2066
+ });
2067
+ }
2068
+ } else {
2069
+ i++;
1907
2070
  }
1908
2071
  }
1909
- );
2072
+ return blocks;
2073
+ }
2074
+ function getPlainText(node) {
2075
+ if (node.type === "text" && node.text !== void 0) return node.text;
2076
+ if (!node.content) return "";
2077
+ return node.content.map(getPlainText).join("");
2078
+ }
2079
+ function convertCheckboxesToTaskList(node, blocks) {
2080
+ const cursor = { index: 0 };
2081
+ return convertCheckboxesRecursive(node, blocks, cursor);
2082
+ }
2083
+ function convertCheckboxesRecursive(node, blocks, cursor) {
2084
+ var _a;
2085
+ if (node.type === "bulletList" && node.content && node.content.length > 0 && cursor.index < blocks.length) {
2086
+ const block = blocks[cursor.index];
2087
+ if (!block) return node;
2088
+ if (node.content.length === block.states.length) {
2089
+ const plaintexts = node.content.map((listItem) => getPlainText(listItem));
2090
+ const matches = plaintexts.every((text, i) => text === block.texts[i]);
2091
+ if (matches) {
2092
+ const allSimple = node.content.every((item) => {
2093
+ var _a2, _b;
2094
+ return ((_a2 = item.content) == null ? void 0 : _a2.length) === 1 && ((_b = item.content[0]) == null ? void 0 : _b.type) === "paragraph";
2095
+ });
2096
+ if (!allSimple) {
2097
+ cursor.index++;
2098
+ return node;
2099
+ }
2100
+ cursor.index++;
2101
+ node.type = "taskList";
2102
+ node.attrs = { localId: `tasklist-${++taskIdCounter}` };
2103
+ for (const [i, listItem] of node.content.entries()) {
2104
+ listItem.type = "taskItem";
2105
+ listItem.attrs = {
2106
+ localId: `task-${++taskIdCounter}`,
2107
+ state: block.states[i]
2108
+ };
2109
+ const firstChild = (_a = listItem.content) == null ? void 0 : _a[0];
2110
+ if ((firstChild == null ? void 0 : firstChild.type) === "paragraph" && firstChild.content) {
2111
+ listItem.content = firstChild.content;
2112
+ }
2113
+ }
2114
+ return node;
2115
+ }
2116
+ }
2117
+ }
2118
+ if (node.content && Array.isArray(node.content)) {
2119
+ node.content = node.content.map((child) => convertCheckboxesRecursive(child, blocks, cursor));
2120
+ }
2121
+ return node;
2122
+ }
2123
+ function convertDetailsToExpandSyntax(markdown) {
2124
+ if (!markdown) return markdown;
2125
+ let previousText = "";
2126
+ let currentText = markdown;
2127
+ while (previousText !== currentText) {
2128
+ previousText = currentText;
2129
+ currentText = currentText.replace(
2130
+ /<details[^>]*>\s*<summary[^>]*>([\s\S]*?)<\/summary>([\s\S]*?)<\/details>/gi,
2131
+ (_match, summary, content) => {
2132
+ const cleanSummary = summary.trim().replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
2133
+ let cleanContent = content.trim();
2134
+ cleanContent = cleanContent.replace(/\n{3,}/g, "\n\n");
2135
+ if (cleanContent) {
2136
+ return `~~~expand title="${cleanSummary}"
2137
+ ${cleanContent}
2138
+ ~~~`;
2139
+ } else {
2140
+ return `~~~expand title="${cleanSummary}"
2141
+ ~~~`;
2142
+ }
2143
+ }
2144
+ );
2145
+ }
2146
+ return currentText;
2147
+ }
2148
+ function adfToMarkdown(adf) {
2149
+ if (!adf) return "";
2150
+ if (typeof adf === "string") return adf;
2151
+ return parser.adfToMarkdown(adf);
2152
+ }
2153
+ function markdownToAdf(markdown) {
2154
+ if (!markdown) {
2155
+ return { type: "doc", version: 1, content: [] };
2156
+ }
2157
+ taskIdCounter = 0;
2158
+ const checkboxBlocks = extractCheckboxBlocks(markdown);
2159
+ const preprocessed = convertDetailsToExpandSyntax(markdown);
2160
+ const adf = parser.markdownToAdf(preprocessed);
2161
+ let result = sanitizeCodeMarks(adf);
2162
+ result = wrapTableCellContent(result);
2163
+ result = convertCheckboxesToTaskList(result, checkboxBlocks);
2164
+ return result;
2165
+ }
2166
+
2167
+ // src/lib/providers/jira/JiraApiClient.ts
2168
+ var JiraApiClient = class {
2169
+ constructor(config) {
2170
+ this.baseUrl = `${config.host.replace(/\/$/, "")}/rest/api/3`;
2171
+ const credentials = Buffer.from(`${config.username}:${config.apiToken}`).toString("base64");
2172
+ this.authHeader = `Basic ${credentials}`;
2173
+ }
2174
+ /**
2175
+ * Make an HTTP request to Jira API
2176
+ */
2177
+ async request(method, endpoint, body) {
2178
+ const url = new URL(`${this.baseUrl}${endpoint}`);
2179
+ getLogger().debug(`Jira API ${method} request`, { url: url.toString() });
2180
+ if (body) {
2181
+ getLogger().debug("Jira API request body", JSON.stringify(body, null, 2));
2182
+ }
2183
+ return new Promise((resolve, reject) => {
2184
+ const options = {
2185
+ hostname: url.hostname,
2186
+ port: url.port || 443,
2187
+ path: url.pathname + url.search,
2188
+ method,
2189
+ headers: {
2190
+ "Authorization": this.authHeader,
2191
+ "Accept": "application/json",
2192
+ "Content-Type": "application/json"
2193
+ }
2194
+ };
2195
+ const req = https.request({ ...options, timeout: 3e4 }, (res) => {
2196
+ const chunks = [];
2197
+ res.on("data", (chunk) => {
2198
+ chunks.push(chunk);
2199
+ });
2200
+ res.on("end", () => {
2201
+ var _a;
2202
+ const data = Buffer.concat(chunks).toString("utf8");
2203
+ if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {
2204
+ let errorDetail = data;
2205
+ try {
2206
+ const parsed = JSON.parse(data);
2207
+ const parts = [];
2208
+ if ((_a = parsed.errorMessages) == null ? void 0 : _a.length) {
2209
+ parts.push(`messages: ${parsed.errorMessages.join(", ")}`);
2210
+ }
2211
+ if (parsed.errors && Object.keys(parsed.errors).length) {
2212
+ parts.push(`field errors: ${JSON.stringify(parsed.errors)}`);
2213
+ }
2214
+ if (parts.length) {
2215
+ errorDetail = parts.join("; ");
2216
+ }
2217
+ } catch {
2218
+ }
2219
+ reject(new Error(`Jira API error (${res.statusCode}): ${errorDetail}`));
2220
+ return;
2221
+ }
2222
+ if (res.statusCode === 204 || !data) {
2223
+ resolve({});
2224
+ return;
2225
+ }
2226
+ try {
2227
+ resolve(JSON.parse(data));
2228
+ } catch (error) {
2229
+ reject(new Error(`Failed to parse Jira API response: ${error}`));
2230
+ }
2231
+ });
2232
+ });
2233
+ req.on("timeout", () => {
2234
+ req.destroy();
2235
+ reject(new Error("Jira API request timed out after 30 seconds"));
2236
+ });
2237
+ req.on("error", (error) => {
2238
+ reject(new Error(`Jira API request failed: ${error.message}`));
2239
+ });
2240
+ if (body) {
2241
+ req.write(JSON.stringify(body));
2242
+ }
2243
+ req.end();
2244
+ });
2245
+ }
2246
+ /**
2247
+ * Make a GET request to Jira API
2248
+ */
2249
+ async get(endpoint) {
2250
+ return this.request("GET", endpoint);
2251
+ }
2252
+ /**
2253
+ * Make a POST request to Jira API
2254
+ */
2255
+ async post(endpoint, body) {
2256
+ return this.request("POST", endpoint, body);
2257
+ }
2258
+ /**
2259
+ * Make a PUT request to Jira API
2260
+ */
2261
+ async put(endpoint, body) {
2262
+ return this.request("PUT", endpoint, body);
2263
+ }
2264
+ /**
2265
+ * Make a DELETE request to Jira API
2266
+ */
2267
+ async delete(endpoint) {
2268
+ await this.request("DELETE", endpoint);
2269
+ }
2270
+ /**
2271
+ * Fetch an issue by key (e.g., "PROJ-123")
2272
+ */
2273
+ async getIssue(issueKey) {
2274
+ return this.get(`/issue/${issueKey}`);
2275
+ }
2276
+ /**
2277
+ * Add a comment to an issue
2278
+ * Accepts Markdown content which is converted to ADF for Jira
2279
+ */
2280
+ async addComment(issueKey, body) {
2281
+ const adfBody = markdownToAdf(body);
2282
+ getLogger().debug("Adding comment to Jira issue", { issueKey, bodyLength: body.length });
2283
+ return this.post(`/issue/${issueKey}/comment`, {
2284
+ body: adfBody
2285
+ });
2286
+ }
2287
+ /**
2288
+ * Get all comments for an issue
2289
+ */
2290
+ async getComments(issueKey) {
2291
+ const response = await this.get(`/issue/${issueKey}/comment?maxResults=5000`);
2292
+ if (response.total > response.comments.length) {
2293
+ getLogger().warn(`Comments truncated for issue ${issueKey}: returned ${response.comments.length} of ${response.total} total comments`);
2294
+ }
2295
+ return response.comments;
2296
+ }
2297
+ /**
2298
+ * Update a comment on an issue
2299
+ * Accepts Markdown content which is converted to ADF for Jira
2300
+ */
2301
+ async updateComment(issueKey, commentId, body) {
2302
+ return this.put(`/issue/${issueKey}/comment/${commentId}`, {
2303
+ body: markdownToAdf(body)
2304
+ });
2305
+ }
2306
+ /**
2307
+ * Get available transitions for an issue
2308
+ */
2309
+ async getTransitions(issueKey) {
2310
+ const response = await this.get(`/issue/${issueKey}/transitions`);
2311
+ return response.transitions;
2312
+ }
2313
+ /**
2314
+ * Transition an issue to a new state
2315
+ */
2316
+ async transitionIssue(issueKey, transitionId) {
2317
+ await this.post(`/issue/${issueKey}/transitions`, {
2318
+ transition: {
2319
+ id: transitionId
2320
+ }
2321
+ });
2322
+ }
2323
+ /**
2324
+ * Create a new issue
2325
+ * Accepts Markdown description which is converted to ADF for Jira
2326
+ */
2327
+ async createIssue(projectKey, summary, description, issueType = "Task") {
2328
+ return this.post("/issue", {
2329
+ fields: {
2330
+ project: {
2331
+ key: projectKey
2332
+ },
2333
+ summary,
2334
+ description: markdownToAdf(description),
2335
+ issuetype: {
2336
+ name: issueType
2337
+ }
2338
+ }
2339
+ });
2340
+ }
2341
+ /**
2342
+ * Update an issue's fields (summary, description)
2343
+ * @param issueKey - Jira issue key (e.g., "PROJ-123")
2344
+ * @param fields - Fields to update
2345
+ */
2346
+ async updateIssue(issueKey, fields) {
2347
+ const updateFields = {};
2348
+ if (fields.summary !== void 0) {
2349
+ updateFields.summary = fields.summary;
2350
+ }
2351
+ if (fields.description !== void 0) {
2352
+ updateFields.description = markdownToAdf(fields.description);
2353
+ }
2354
+ await this.put(`/issue/${issueKey}`, { fields: updateFields });
2355
+ }
2356
+ /**
2357
+ * Create an issue with a parent (subtask or child issue)
2358
+ * Accepts Markdown description which is converted to ADF for Jira
2359
+ */
2360
+ async createIssueWithParent(projectKey, summary, description, parentKey, issueType = "Subtask") {
2361
+ return this.post("/issue", {
2362
+ fields: {
2363
+ project: {
2364
+ key: projectKey
2365
+ },
2366
+ summary,
2367
+ description: markdownToAdf(description),
2368
+ issuetype: {
2369
+ name: issueType
2370
+ },
2371
+ parent: {
2372
+ key: parentKey
2373
+ }
2374
+ }
2375
+ });
2376
+ }
2377
+ /**
2378
+ * Create an issue link (dependency/relationship between issues)
2379
+ * @param inwardKey - The issue key for the inward side (e.g., the blocked issue)
2380
+ * @param outwardKey - The issue key for the outward side (e.g., the blocking issue)
2381
+ * @param linkType - The link type name (e.g., "Blocks")
2382
+ */
2383
+ async createIssueLink(inwardKey, outwardKey, linkType) {
2384
+ await this.post("/issueLink", {
2385
+ type: {
2386
+ name: linkType
2387
+ },
2388
+ inwardIssue: {
2389
+ key: inwardKey
2390
+ },
2391
+ outwardIssue: {
2392
+ key: outwardKey
2393
+ }
2394
+ });
2395
+ }
2396
+ /**
2397
+ * Delete an issue link by ID
2398
+ */
2399
+ async deleteIssueLink(linkId) {
2400
+ await this.delete(`/issueLink/${linkId}`);
2401
+ }
2402
+ /**
2403
+ * Search issues using JQL
2404
+ * Automatically paginates through all results up to MAX_SEARCH_RESULTS.
2405
+ */
2406
+ async searchIssues(jql) {
2407
+ const MAX_SEARCH_RESULTS = 5e3;
2408
+ const allIssues = [];
2409
+ let nextPageToken;
2410
+ const maxResults = 100;
2411
+ while (allIssues.length < MAX_SEARCH_RESULTS) {
2412
+ const body = {
2413
+ jql,
2414
+ maxResults,
2415
+ fields: [
2416
+ "summary",
2417
+ "description",
2418
+ "status",
2419
+ "issuetype",
2420
+ "project",
2421
+ "assignee",
2422
+ "reporter",
2423
+ "labels",
2424
+ "created",
2425
+ "updated",
2426
+ "issuelinks",
2427
+ "parent"
2428
+ ]
2429
+ };
2430
+ if (nextPageToken) {
2431
+ body.nextPageToken = nextPageToken;
2432
+ }
2433
+ const response = await this.post(
2434
+ "/search/jql",
2435
+ body
2436
+ );
2437
+ allIssues.push(...response.issues);
2438
+ if (!response.nextPageToken || response.issues.length === 0) {
2439
+ break;
2440
+ }
2441
+ nextPageToken = response.nextPageToken;
2442
+ }
2443
+ if (allIssues.length >= MAX_SEARCH_RESULTS) {
2444
+ getLogger().warn(`Search results truncated at ${MAX_SEARCH_RESULTS} issues. The query matched more results than the safety cap allows.`, { jql, returnedCount: allIssues.length });
2445
+ }
2446
+ return allIssues;
2447
+ }
2448
+ /**
2449
+ * Test connection to Jira API
2450
+ */
2451
+ async testConnection() {
2452
+ try {
2453
+ await this.get("/myself");
2454
+ return true;
2455
+ } catch (error) {
2456
+ const message = error instanceof Error ? error.message : String(error);
2457
+ if (message.includes("Jira API error (401)") || message.includes("Jira API error (403)")) {
2458
+ getLogger().error("Jira connection test failed: authentication error", { error });
2459
+ return false;
2460
+ }
2461
+ throw error;
2462
+ }
2463
+ }
2464
+ };
2465
+
2466
+ // src/utils/prompt.ts
2467
+ import * as readline from "readline";
2468
+
2469
+ // src/utils/notification.ts
2470
+ import net from "net";
2471
+ import fs from "fs";
2472
+ import path from "path";
2473
+ import os from "os";
2474
+ import { execSync } from "child_process";
2475
+ var DEBUG = process.env.ILOOM_NOTIF_DEBUG === "1";
2476
+
2477
+ // src/utils/prompt.ts
2478
+ async function promptConfirmation(message, defaultValue = false) {
2479
+ const suffix = defaultValue ? "[Y/n]" : "[y/N]";
2480
+ const fullMessage = `${message} ${suffix}: `;
2481
+ while (true) {
2482
+ const rl = readline.createInterface({
2483
+ input: process.stdin,
2484
+ output: process.stdout
2485
+ });
2486
+ const answer = await new Promise((resolve) => {
2487
+ rl.question(fullMessage, (ans) => {
2488
+ rl.close();
2489
+ resolve(ans);
2490
+ });
2491
+ });
2492
+ const normalized = answer.trim().toLowerCase();
2493
+ if (normalized === "") {
2494
+ return defaultValue;
2495
+ }
2496
+ if (normalized === "y" || normalized === "yes") {
2497
+ return true;
2498
+ }
2499
+ if (normalized === "n" || normalized === "no") {
2500
+ return false;
2501
+ }
2502
+ logger.warn("Invalid input. Please enter y/yes or n/no.");
2503
+ }
2504
+ }
2505
+
2506
+ // src/lib/providers/jira/JiraIssueTracker.ts
2507
+ var JiraIssueTracker = class {
2508
+ constructor(config, options) {
2509
+ this.providerName = "jira";
2510
+ this.supportsPullRequests = false;
2511
+ this.config = config;
2512
+ this.client = new JiraApiClient({
2513
+ host: config.host,
2514
+ username: config.username,
2515
+ apiToken: config.apiToken
2516
+ });
2517
+ this.prompter = (options == null ? void 0 : options.prompter) ?? promptConfirmation;
2518
+ }
2519
+ /**
2520
+ * Normalize identifier to canonical uppercase form
2521
+ * Jira issue keys are case-sensitive in the API (must be uppercase)
2522
+ */
2523
+ normalizeIdentifier(identifier) {
2524
+ return String(identifier).toUpperCase();
2525
+ }
2526
+ /**
2527
+ * Detect input type from user input
2528
+ * Jira issues follow pattern: PROJECTKEY-123 (case-insensitive)
2529
+ */
2530
+ async detectInputType(input) {
2531
+ const jiraPattern = /^([A-Z][A-Z0-9]+)-(\d+)$/i;
2532
+ const match = input.match(jiraPattern);
2533
+ if (!match) {
2534
+ return { type: "unknown", identifier: null, rawInput: input };
2535
+ }
2536
+ const issueKey = this.normalizeIdentifier(input);
2537
+ getLogger().debug("Checking if input is a Jira issue", { issueKey });
2538
+ try {
2539
+ await this.client.getIssue(issueKey);
2540
+ return { type: "issue", identifier: issueKey, rawInput: input };
2541
+ } catch (error) {
2542
+ if (error instanceof Error && (/404/.test(error.message) || /not found/i.test(error.message))) {
2543
+ getLogger().debug("Issue not found", { issueKey, error });
2544
+ return { type: "unknown", identifier: null, rawInput: input };
2545
+ }
2546
+ throw error;
2547
+ }
2548
+ }
2549
+ /**
2550
+ * Fetch issue details
2551
+ */
2552
+ async fetchIssue(identifier) {
2553
+ const issueKey = this.normalizeIdentifier(identifier);
2554
+ getLogger().debug("Fetching Jira issue", { issueKey });
2555
+ const jiraIssue = await this.client.getIssue(issueKey);
2556
+ return this.mapJiraIssueToIssue(jiraIssue);
2557
+ }
2558
+ /**
2559
+ * Check if issue exists (silent validation)
2560
+ */
2561
+ async isValidIssue(identifier) {
2562
+ try {
2563
+ return await this.fetchIssue(identifier);
2564
+ } catch (error) {
2565
+ if (error instanceof Error && (/404/.test(error.message) || /not found/i.test(error.message))) {
2566
+ getLogger().debug("Issue validation failed: not found", { identifier, error });
2567
+ return false;
2568
+ }
2569
+ throw error;
2570
+ }
2571
+ }
2572
+ /**
2573
+ * Validate issue state
2574
+ * Note: Jira doesn't have a simple "closed" state - depends on workflow
2575
+ */
2576
+ async validateIssueState(issue) {
2577
+ getLogger().debug("Jira issue state", { issueKey: issue.number, state: issue.state });
2578
+ if (issue.state === "closed") {
2579
+ const shouldContinue = await this.prompter(
2580
+ `Issue ${issue.number} is in a completed state. Continue anyway?`
2581
+ );
2582
+ if (!shouldContinue) {
2583
+ throw new Error("User cancelled due to completed issue");
2584
+ }
2585
+ }
2586
+ }
2587
+ /**
2588
+ * Create a new issue
2589
+ */
2590
+ async createIssue(title, body, _repository, _labels) {
2591
+ getLogger().debug("Creating Jira issue", { title, projectKey: this.config.projectKey });
2592
+ const jiraIssue = await this.client.createIssue(
2593
+ this.config.projectKey,
2594
+ title,
2595
+ body,
2596
+ this.config.defaultIssueType
2597
+ );
2598
+ return {
2599
+ number: jiraIssue.key,
2600
+ url: `${this.config.host}/browse/${jiraIssue.key}`
2601
+ };
2602
+ }
2603
+ /**
2604
+ * Get issue URL
2605
+ */
2606
+ async getIssueUrl(identifier) {
2607
+ const issueKey = this.normalizeIdentifier(identifier);
2608
+ return `${this.config.host}/browse/${issueKey}`;
2609
+ }
2610
+ /**
2611
+ * Move issue to "In Progress" state
2612
+ * Uses configured transition mapping or default transition name
2613
+ */
2614
+ async moveIssueToInProgress(identifier) {
2615
+ var _a;
2616
+ const issueKey = this.normalizeIdentifier(identifier);
2617
+ getLogger().debug("Moving Jira issue to In Progress", { issueKey });
2618
+ const transitions = await this.client.getTransitions(issueKey);
2619
+ const transitionName = ((_a = this.config.transitionMappings) == null ? void 0 : _a["In Progress"]) ?? this.findTransitionByName(transitions, ["In Progress", "Start Progress", "Start"]);
2620
+ if (!transitionName) {
2621
+ throw new Error(
2622
+ `Could not find "In Progress" transition for ${issueKey}. Available transitions: ${transitions.map((t) => t.name).join(", ")}. Configure custom mapping in settings.json: issueManagement.jira.transitionMappings`
2623
+ );
2624
+ }
2625
+ const transition = transitions.find((t) => t.name === transitionName);
2626
+ if (!transition) {
2627
+ throw new Error(`Transition "${transitionName}" not found`);
2628
+ }
2629
+ await this.client.transitionIssue(issueKey, transition.id);
2630
+ getLogger().info("Issue transitioned successfully", { issueKey, transition: transitionName });
2631
+ }
2632
+ /**
2633
+ * Move issue to "Ready for Review" state
2634
+ * Uses configured transition mapping or default transition name
2635
+ */
2636
+ async moveIssueToReadyForReview(identifier) {
2637
+ var _a;
2638
+ const issueKey = this.normalizeIdentifier(identifier);
2639
+ getLogger().debug("Moving Jira issue to Ready for Review", { issueKey });
2640
+ const transitions = await this.client.getTransitions(issueKey);
2641
+ const transitionName = ((_a = this.config.transitionMappings) == null ? void 0 : _a["Ready for Review"]) ?? this.findTransitionByName(transitions, ["Ready for Review", "In Review", "Code Review", "Review"]);
2642
+ if (!transitionName) {
2643
+ throw new Error(
2644
+ `Could not find "Ready for Review" transition for ${issueKey}. Available transitions: ${transitions.map((t) => t.name).join(", ")}. Configure custom mapping in settings.json: issueManagement.jira.transitionMappings`
2645
+ );
2646
+ }
2647
+ const transition = transitions.find((t) => t.name === transitionName);
2648
+ if (!transition) {
2649
+ throw new Error(`Transition "${transitionName}" not found`);
2650
+ }
2651
+ await this.client.transitionIssue(issueKey, transition.id);
2652
+ getLogger().info("Issue transitioned to Ready for Review", { issueKey, transition: transitionName });
2653
+ }
2654
+ /**
2655
+ * Close an issue by transitioning to "Done" state
2656
+ * Uses configured transition mapping or default transition names
2657
+ */
2658
+ async closeIssue(identifier) {
2659
+ var _a;
2660
+ const issueKey = this.normalizeIdentifier(identifier);
2661
+ getLogger().debug("Closing Jira issue", { issueKey });
2662
+ const transitions = await this.client.getTransitions(issueKey);
2663
+ const transitionName = ((_a = this.config.transitionMappings) == null ? void 0 : _a["Done"]) ?? this.findTransitionByName(transitions, ["Done", "Close", "Closed", "Resolve", "Resolved"]);
2664
+ if (!transitionName) {
2665
+ throw new Error(
2666
+ `Could not find "Done" transition for ${issueKey}. Available transitions: ${transitions.map((t) => t.name).join(", ")}. Configure custom mapping in settings.json: issueManagement.jira.transitionMappings`
2667
+ );
2668
+ }
2669
+ const transition = transitions.find((t) => t.name === transitionName);
2670
+ if (!transition) {
2671
+ throw new Error(`Transition "${transitionName}" not found`);
2672
+ }
2673
+ await this.client.transitionIssue(issueKey, transition.id);
2674
+ getLogger().info("Issue closed successfully", { issueKey, transition: transitionName });
2675
+ }
2676
+ /**
2677
+ * Reopen an issue by transitioning back to an open state
2678
+ * Uses configured transition mapping or default transition names
2679
+ */
2680
+ async reopenIssue(identifier) {
2681
+ var _a;
2682
+ const issueKey = this.normalizeIdentifier(identifier);
2683
+ getLogger().debug("Reopening Jira issue", { issueKey });
2684
+ const transitions = await this.client.getTransitions(issueKey);
2685
+ const transitionName = ((_a = this.config.transitionMappings) == null ? void 0 : _a["Reopen"]) ?? this.findTransitionByName(transitions, ["Reopen", "To Do", "Open", "Backlog"]);
2686
+ if (!transitionName) {
2687
+ throw new Error(
2688
+ `Could not find "Reopen" transition for ${issueKey}. Available transitions: ${transitions.map((t) => t.name).join(", ")}. Configure custom mapping in settings.json: issueManagement.jira.transitionMappings`
2689
+ );
2690
+ }
2691
+ const transition = transitions.find((t) => t.name === transitionName);
2692
+ if (!transition) {
2693
+ throw new Error(`Transition "${transitionName}" not found`);
2694
+ }
2695
+ await this.client.transitionIssue(issueKey, transition.id);
2696
+ getLogger().info("Issue reopened successfully", { issueKey, transition: transitionName });
2697
+ }
2698
+ /**
2699
+ * Extract context from issue for AI prompts
2700
+ */
2701
+ extractContext(entity) {
2702
+ return `Issue: ${entity.number}
2703
+ Title: ${entity.title}
2704
+ Status: ${entity.state}
2705
+ URL: ${entity.url}
2706
+
2707
+ Description:
2708
+ ${entity.body}
2709
+
2710
+ ${entity.labels.length > 0 ? `Labels: ${entity.labels.join(", ")}` : ""}
2711
+ ${entity.assignees.length > 0 ? `Assignees: ${entity.assignees.join(", ")}` : ""}`;
2712
+ }
2713
+ /**
2714
+ * Fetch child issues of a Jira parent issue using JQL
2715
+ * @param parentIdentifier - Jira issue key (e.g., "PROJ-123")
2716
+ * @param _repo - Repository (unused for Jira)
2717
+ * @returns Array of child issues
2718
+ */
2719
+ async getChildIssues(parentIdentifier, _repo) {
2720
+ const parentKey = this.normalizeIdentifier(parentIdentifier);
2721
+ const jiraKeyPattern = /^[A-Z][A-Z0-9]+-\d+$/;
2722
+ if (!jiraKeyPattern.test(parentKey)) {
2723
+ getLogger().warn(`Invalid Jira issue key format: ${parentKey}`);
2724
+ return [];
2725
+ }
2726
+ const issues = await this.client.searchIssues(`parent = ${parentKey}`);
2727
+ return issues.map((issue) => ({
2728
+ id: issue.key,
2729
+ title: issue.fields.summary,
2730
+ url: `${this.config.host}/browse/${issue.key}`,
2731
+ state: issue.fields.status.name.toLowerCase()
2732
+ }));
2733
+ }
2734
+ /**
2735
+ * Get issue details (alias for fetchIssue for MCP compatibility)
2736
+ */
2737
+ async getIssue(identifier) {
2738
+ return this.fetchIssue(identifier);
2739
+ }
2740
+ /**
2741
+ * Get all comments for an issue
2742
+ */
2743
+ async getComments(identifier) {
2744
+ const issueKey = this.normalizeIdentifier(identifier);
2745
+ getLogger().debug("Fetching Jira comments", { issueKey });
2746
+ const comments = await this.client.getComments(issueKey);
2747
+ return comments.map((comment) => ({
2748
+ id: comment.id,
2749
+ body: adfToMarkdown(comment.body),
2750
+ author: comment.author,
2751
+ createdAt: comment.created,
2752
+ updatedAt: comment.updated
2753
+ }));
2754
+ }
2755
+ /**
2756
+ * Add a comment to an issue
2757
+ */
2758
+ async addComment(identifier, body) {
2759
+ const issueKey = this.normalizeIdentifier(identifier);
2760
+ getLogger().debug("Adding Jira comment", { issueKey });
2761
+ const comment = await this.client.addComment(issueKey, body);
2762
+ return { id: comment.id };
2763
+ }
2764
+ /**
2765
+ * Update an existing comment
2766
+ */
2767
+ async updateComment(identifier, commentId, body) {
2768
+ const issueKey = this.normalizeIdentifier(identifier);
2769
+ getLogger().debug("Updating Jira comment", { issueKey, commentId });
2770
+ await this.client.updateComment(issueKey, commentId, body);
2771
+ }
2772
+ /**
2773
+ * Get the underlying API client (for direct API access by MCP provider)
2774
+ */
2775
+ getApiClient() {
2776
+ return this.client;
2777
+ }
2778
+ /**
2779
+ * Get configuration (for MCP provider)
2780
+ */
2781
+ getConfig() {
2782
+ return this.config;
2783
+ }
2784
+ /**
2785
+ * Map Jira API issue to generic Issue type
2786
+ */
2787
+ mapJiraIssueToIssue(jiraIssue) {
2788
+ const description = adfToMarkdown(jiraIssue.fields.description);
2789
+ return {
2790
+ id: jiraIssue.id,
2791
+ key: jiraIssue.key,
2792
+ number: jiraIssue.key,
2793
+ title: jiraIssue.fields.summary,
2794
+ body: description,
2795
+ state: this.mapJiraStatusToState(jiraIssue.fields.status.name),
2796
+ labels: jiraIssue.fields.labels,
2797
+ assignees: jiraIssue.fields.assignee ? [jiraIssue.fields.assignee.displayName] : [],
2798
+ assignee: jiraIssue.fields.assignee,
2799
+ author: jiraIssue.fields.reporter,
2800
+ url: `${this.config.host}/browse/${jiraIssue.key}`,
2801
+ issueType: jiraIssue.fields.issuetype.name,
2802
+ status: jiraIssue.fields.status.name
2803
+ };
2804
+ }
2805
+ mapJiraStatusToState(statusName) {
2806
+ const normalized = statusName.toLowerCase();
2807
+ const closedStatuses = ["done", "closed", "resolved", "cancelled", "canceled"];
2808
+ return closedStatuses.includes(normalized) ? "closed" : "open";
2809
+ }
2810
+ /**
2811
+ * Find a transition by name, trying multiple possible names
2812
+ */
2813
+ findTransitionByName(transitions, names) {
2814
+ for (const name of names) {
2815
+ const transition = transitions.find(
2816
+ (t) => t.name.toLowerCase() === name.toLowerCase()
2817
+ );
2818
+ if (transition) {
2819
+ return transition.name;
2820
+ }
2821
+ }
2822
+ return null;
2823
+ }
2824
+ };
2825
+
2826
+ // src/lib/SettingsManager.ts
2827
+ import { readFile } from "fs/promises";
2828
+ import path2 from "path";
2829
+ import os2 from "os";
2830
+ import { z } from "zod";
2831
+ import deepmerge from "deepmerge";
2832
+ var BaseAgentSettingsSchema = z.object({
2833
+ model: z.enum(["sonnet", "opus", "haiku"]).optional().describe("Claude model shorthand: sonnet, opus, or haiku"),
2834
+ enabled: z.boolean().optional().describe("Whether this agent is enabled. Defaults to true."),
2835
+ providers: z.record(
2836
+ z.enum(["claude", "gemini", "codex"]),
2837
+ z.string()
2838
+ ).optional().describe('Map of review providers to model names. Keys: claude, gemini, codex. Values: model name strings (e.g., "sonnet", "gemini-3-pro-preview", "gpt-5.2-codex")'),
2839
+ review: z.boolean().optional().describe("Whether artifacts from this agent should be reviewed before posting (defaults to false)")
2840
+ });
2841
+ var AgentSettingsSchema = BaseAgentSettingsSchema.extend({
2842
+ agents: z.record(z.string(), BaseAgentSettingsSchema).optional().describe("Nested per-agent model overrides for swarm mode. Configure under agents.iloom-swarm-worker.agents.<agent-name>.model to set a different model for phase agents when running inside swarm workers. Fallback chain: swarm-specific agent model > explicit swarm worker model > base agent model. Only meaningful under the iloom-swarm-worker agent entry.")
2843
+ });
2844
+ var SpinAgentSettingsSchema = z.object({
2845
+ model: z.enum(["sonnet", "opus", "haiku"]).default("opus").describe("Claude model shorthand for spin orchestrator")
2846
+ });
2847
+ var PlanCommandSettingsSchema = z.object({
2848
+ model: z.enum(["sonnet", "opus", "haiku"]).default("opus").describe("Claude model shorthand for plan command"),
2849
+ planner: z.enum(["claude", "gemini", "codex"]).default("claude").describe("AI provider for creating the plan"),
2850
+ reviewer: z.enum(["claude", "gemini", "codex", "none"]).default("none").describe("AI provider for reviewing the plan (none to skip review)")
2851
+ });
2852
+ var SummarySettingsSchema = z.object({
2853
+ model: z.enum(["sonnet", "opus", "haiku"]).default("sonnet").describe("Claude model shorthand for session summary generation")
2854
+ });
2855
+ var WorkflowPermissionSchema = z.object({
2856
+ permissionMode: z.enum(["plan", "acceptEdits", "bypassPermissions", "default"]).optional().describe("Permission mode for Claude CLI in this workflow type"),
2857
+ noVerify: z.boolean().optional().describe("Skip pre-commit hooks (--no-verify) when committing during commit and finish workflows"),
2858
+ startIde: z.boolean().default(true).describe("Launch IDE (code) when starting this workflow type"),
2859
+ startDevServer: z.boolean().default(true).describe("Launch development server when starting this workflow type"),
2860
+ startAiAgent: z.boolean().default(true).describe("Launch Claude Code agent when starting this workflow type"),
2861
+ startTerminal: z.boolean().default(false).describe("Launch terminal window without dev server when starting this workflow type"),
2862
+ generateSummary: z.boolean().default(true).describe("Generate and post Claude session summary when finishing this workflow type")
2863
+ });
2864
+ var WorkflowPermissionSchemaNoDefaults = z.object({
2865
+ permissionMode: z.enum(["plan", "acceptEdits", "bypassPermissions", "default"]).optional().describe("Permission mode for Claude CLI in this workflow type"),
2866
+ noVerify: z.boolean().optional().describe("Skip pre-commit hooks (--no-verify) when committing during commit and finish workflows"),
2867
+ startIde: z.boolean().optional().describe("Launch IDE (code) when starting this workflow type"),
2868
+ startDevServer: z.boolean().optional().describe("Launch development server when starting this workflow type"),
2869
+ startAiAgent: z.boolean().optional().describe("Launch Claude Code agent when starting this workflow type"),
2870
+ startTerminal: z.boolean().optional().describe("Launch terminal window without dev server when starting this workflow type"),
2871
+ generateSummary: z.boolean().optional().describe("Generate and post Claude session summary when finishing this workflow type")
2872
+ });
2873
+ var WorkflowsSettingsSchema = z.object({
2874
+ issue: WorkflowPermissionSchema.optional(),
2875
+ pr: WorkflowPermissionSchema.optional(),
2876
+ regular: WorkflowPermissionSchema.optional()
2877
+ }).optional();
2878
+ var WorkflowsSettingsSchemaNoDefaults = z.object({
2879
+ issue: WorkflowPermissionSchemaNoDefaults.optional(),
2880
+ pr: WorkflowPermissionSchemaNoDefaults.optional(),
2881
+ regular: WorkflowPermissionSchemaNoDefaults.optional()
2882
+ }).optional();
2883
+ var CapabilitiesSettingsSchema = z.object({
2884
+ web: z.object({
2885
+ basePort: z.number().min(1, "Base port must be >= 1").max(65535, "Base port must be <= 65535").optional().describe("Base port for web workspace port calculations (default: 3000)")
2886
+ }).optional().describe('Web dev server settings. To declare a project as a web project, add "web" to the capabilities array in .iloom/package.iloom.json or .iloom/package.iloom.local.json.'),
2887
+ database: z.object({
2888
+ databaseUrlEnvVarName: z.string().min(1, "Database URL variable name cannot be empty").regex(/^[A-Z_][A-Z0-9_]*$/, "Must be valid env var name (uppercase, underscores)").optional().default("DATABASE_URL").describe("Name of environment variable for database connection URL")
2889
+ }).optional()
2890
+ }).optional();
2891
+ var CapabilitiesSettingsSchemaNoDefaults = z.object({
2892
+ web: z.object({
2893
+ basePort: z.number().min(1, "Base port must be >= 1").max(65535, "Base port must be <= 65535").optional().describe("Base port for web workspace port calculations (default: 3000)")
2894
+ }).optional().describe('Web dev server settings. To declare a project as a web project, add "web" to the capabilities array in .iloom/package.iloom.json or .iloom/package.iloom.local.json.'),
2895
+ database: z.object({
2896
+ databaseUrlEnvVarName: z.string().min(1, "Database URL variable name cannot be empty").regex(/^[A-Z_][A-Z0-9_]*$/, "Must be valid env var name (uppercase, underscores)").optional().describe("Name of environment variable for database connection URL")
2897
+ }).optional()
2898
+ }).optional();
2899
+ var NeonSettingsSchema = z.object({
2900
+ projectId: z.string().min(1).regex(/^[a-zA-Z0-9-]+$/, "Neon project ID must contain only letters, numbers, and hyphens").describe('Neon project ID found in your project URL (e.g., "fantastic-fox-3566354")'),
2901
+ parentBranch: z.string().min(1).describe("Branch from which new database branches are created")
2902
+ });
2903
+ var DatabaseProvidersSettingsSchema = z.object({
2904
+ neon: NeonSettingsSchema.optional().describe(
2905
+ "Neon database configuration. Requires Neon CLI installed and authenticated for database branching."
2906
+ )
2907
+ }).optional();
2908
+ var IloomSettingsSchema = z.object({
2909
+ mainBranch: z.string().min(1, "Settings 'mainBranch' cannot be empty").optional().describe("Name of the main/primary branch for the repository"),
2910
+ sourceEnvOnStart: z.boolean().default(false).describe(
2911
+ "Source all dotenv-flow files (.env, .env.local, .env.development, .env.development.local) when launching terminal processes (Claude, dev server, terminal). Files are sourced in precedence order so later files override earlier ones. NOTE TO AI AGENTS: Before making changes to settings files, YOU MUST OFFER to check .env file compatibility. WARNING: Unquoted values in .env files may break sourcing in bash/zsh shells. Before enabling, verify ALL your .env.* files do not contain unquoted special characters (e.g., database URLs with ?, &, or other shell metacharacters). Shell compatibility issues may cause processes to fail or behave unexpectedly."
2912
+ ),
2913
+ worktreePrefix: z.string().optional().refine(
2914
+ (val) => {
2915
+ if (val === void 0) return true;
2916
+ if (val === "") return true;
2917
+ const allowedChars = /^[a-zA-Z0-9\-_/]+$/;
2918
+ if (!allowedChars.test(val)) return false;
2919
+ if (/^[-_/]+$/.test(val)) return false;
2920
+ const segments = val.split("/");
2921
+ for (const segment of segments) {
2922
+ if (segment && /^[-_]+$/.test(segment)) {
2923
+ return false;
2924
+ }
2925
+ }
2926
+ return true;
2927
+ },
2928
+ {
2929
+ message: "worktreePrefix contains invalid characters. Only alphanumeric characters, hyphens (-), underscores (_), and forward slashes (/) are allowed. Use forward slashes for nested directories."
2930
+ }
2931
+ ).describe(
2932
+ "Prefix for worktree directories. Empty string disables prefix. Defaults to <repo-name>-looms if not set."
2933
+ ),
2934
+ protectedBranches: z.array(z.string().min(1, "Protected branch name cannot be empty")).optional().describe('List of branches that cannot be deleted (defaults to [mainBranch, "main", "master", "develop"])'),
2935
+ copyGitIgnoredPatterns: z.array(z.string().min(1, "Pattern cannot be empty")).optional().describe(`Glob patterns for gitignored files to copy to looms (e.g., ["*.db", "data/*.sqlite"]). Great for local dbs and large test data files that are too big to commit to git. Note: .env (dotenv-flow) files, iloom's and claude's local settings are automatically copied and do not need to be specified here.`),
2936
+ workflows: WorkflowsSettingsSchema.describe("Per-workflow-type permission configurations"),
2937
+ agents: z.record(z.string(), AgentSettingsSchema).optional().nullable().describe(
2938
+ 'Per-agent configuration overrides. Available agents: iloom-issue-analyzer (analyzes issues), iloom-issue-planner (creates implementation plans), iloom-issue-analyze-and-plan (combined analysis and planning), iloom-issue-complexity-evaluator (evaluates complexity), iloom-issue-enhancer (enhances issue descriptions), iloom-issue-implementer (implements code changes), iloom-code-reviewer (reviews code changes against requirements), iloom-artifact-reviewer (reviews artifacts before posting), iloom-swarm-worker (swarm worker agent, dynamically generated). The iloom-swarm-worker agent supports a nested "agents" sub-record for configuring phase agent models specifically in swarm mode.'
2939
+ ),
2940
+ spin: SpinAgentSettingsSchema.optional().describe(
2941
+ "Spin orchestrator configuration. Model defaults to opus when not configured."
2942
+ ),
2943
+ plan: PlanCommandSettingsSchema.optional().describe(
2944
+ "Plan command configuration. Model defaults to opus, planner to claude, reviewer to none when not configured."
2945
+ ),
2946
+ summary: SummarySettingsSchema.optional().describe(
2947
+ "Session summary generation configuration. Model defaults to sonnet when not configured."
2948
+ ),
2949
+ capabilities: CapabilitiesSettingsSchema.describe("Project capability configurations"),
2950
+ databaseProviders: DatabaseProvidersSettingsSchema.describe("Database provider configurations"),
2951
+ issueManagement: z.object({
2952
+ // SYNC: If this default changes, update displayDefaultsBox() in src/utils/first-run-setup.ts
2953
+ provider: z.enum(["github", "linear", "jira"]).optional().default("github").describe("Issue tracker provider (github, linear, jira)"),
2954
+ github: z.object({
2955
+ remote: z.string().min(1, "Remote name cannot be empty").describe("Git remote name to use for GitHub operations")
2956
+ }).optional(),
2957
+ linear: z.object({
2958
+ teamId: z.string().min(1, "Team ID cannot be empty").describe('Linear team identifier (e.g., "ENG", "PLAT")'),
2959
+ branchFormat: z.string().optional().describe("Branch naming template for Linear issues"),
2960
+ apiToken: z.string().optional().describe("Linear API token (lin_api_...). SECURITY: Store in settings.local.json only, never commit to source control.")
2961
+ }).optional(),
2962
+ jira: z.object({
2963
+ host: z.string().min(1, "Jira host cannot be empty").describe('Jira instance URL (e.g., "https://yourcompany.atlassian.net")'),
2964
+ username: z.string().min(1, "Jira username/email cannot be empty").describe("Jira username or email address"),
2965
+ apiToken: z.string().optional().describe("Jira API token. SECURITY: Store in settings.local.json only, never commit to source control. Generate at: https://id.atlassian.com/manage-profile/security/api-tokens"),
2966
+ projectKey: z.string().min(1, "Project key cannot be empty").describe('Jira project key (e.g., "PROJ", "ENG")'),
2967
+ boardId: z.string().optional().describe("Jira board ID for sprint/workflow operations (optional)"),
2968
+ transitionMappings: z.record(z.string(), z.string()).optional().describe('Map iloom states to Jira transition names (e.g., {"In Review": "Start Review"})'),
2969
+ defaultIssueType: z.string().min(1).optional().default("Task").describe('Default Jira issue type name for creating issues (e.g., "Task", "Story", "Bug")'),
2970
+ defaultSubtaskType: z.string().min(1).optional().default("Subtask").describe('Default Jira issue type name for creating subtasks/child issues (e.g., "Subtask", "Sub-task")'),
2971
+ doneStatuses: z.array(z.string()).optional().default(["Done"]).describe('Status names to exclude from issue lists (e.g., ["Done", "Closed", "Verify"])')
2972
+ }).optional()
2973
+ }).optional().describe("Issue management configuration"),
2974
+ mergeBehavior: z.object({
2975
+ // SYNC: If this default changes, update displayDefaultsBox() in src/utils/first-run-setup.ts
2976
+ mode: z.enum(["local", "github-pr", "github-draft-pr"]).default("local"),
2977
+ remote: z.string().optional(),
2978
+ autoCommitPush: z.boolean().optional().describe(
2979
+ "Auto-commit and push after code review in draft PR mode. Defaults to true when mode is github-draft-pr."
2980
+ ),
2981
+ openBrowserOnFinish: z.boolean().default(true).describe(
2982
+ "Open the PR in the default browser after finishing in github-pr or github-draft-pr mode. Use --no-browser flag to override."
2983
+ )
2984
+ }).optional().describe("Merge behavior configuration: local (merge locally), github-pr (create PR), or github-draft-pr (create draft PR at start, mark ready on finish)"),
2985
+ ide: z.object({
2986
+ // SYNC: If this default changes, update displayDefaultsBox() in src/utils/first-run-setup.ts
2987
+ type: z.enum(["vscode", "cursor", "webstorm", "sublime", "intellij", "windsurf", "antigravity"]).default("vscode").describe(
2988
+ "IDE to launch when starting a loom. Options: vscode (Visual Studio Code), cursor (Cursor AI editor), webstorm (JetBrains WebStorm), sublime (Sublime Text), intellij (JetBrains IntelliJ IDEA), windsurf (Windsurf editor), antigravity (Antigravity IDE)."
2989
+ )
2990
+ }).optional().describe(
2991
+ "IDE configuration for workspace launches. Controls which editor opens when you start a loom. Supports VSCode, Cursor, WebStorm, Sublime Text, IntelliJ, Windsurf, and Antigravity. Note: Color synchronization (title bar colors) only works with VSCode-compatible editors (vscode, cursor, windsurf, antigravity)."
2992
+ ),
2993
+ colors: z.object({
2994
+ terminal: z.boolean().default(true).describe("Apply terminal background colors based on branch name (macOS only)"),
2995
+ vscode: z.boolean().default(false).describe(
2996
+ "Apply VSCode/Cursor title bar colors based on branch name. Note: This modifies .vscode/settings.json which may be in source control. Default is false for safety; enable via init or explicitly if .vscode is gitignored."
2997
+ )
2998
+ }).optional().describe("Color synchronization settings for workspace identification"),
2999
+ attribution: z.enum(["off", "upstreamOnly", "on"]).default("upstreamOnly").describe(
3000
+ 'Controls when iloom attribution appears in session summaries. "off" - never show attribution. "upstreamOnly" - only show for contributions to external repositories (e.g., open source). "on" - always show attribution.'
3001
+ ),
3002
+ git: z.object({
3003
+ commitTimeout: z.number().min(1e3, "Commit timeout must be at least 1000ms").max(6e5, "Commit timeout cannot exceed 600000ms (10 minutes)").default(6e4).describe("Timeout in milliseconds for git commit operations. Increase for long-running pre-commit hooks.")
3004
+ }).default({}).describe("Git operation settings")
3005
+ });
3006
+ var IloomSettingsSchemaNoDefaults = z.object({
3007
+ mainBranch: z.string().min(1, "Settings 'mainBranch' cannot be empty").optional().describe("Name of the main/primary branch for the repository"),
3008
+ sourceEnvOnStart: z.boolean().optional().describe(
3009
+ "Source all dotenv-flow files (.env, .env.local, .env.development, .env.development.local) when launching terminal processes (Claude, dev server, terminal). Files are sourced in precedence order so later files override earlier ones. NOTE TO AI AGENTS: Before making changes to settings files, YOU MUST OFFER to check .env compatibility. WARNING: Unquoted values in .env files may break sourcing in bash/zsh shells. Before enabling, verify ALL your .env.* files do not contain unquoted special characters (e.g., database URLs with ?, &, or other shell metacharacters). Shell compatibility issues may cause processes to fail or behave unexpectedly."
3010
+ ),
3011
+ worktreePrefix: z.string().optional().refine(
3012
+ (val) => {
3013
+ if (val === void 0) return true;
3014
+ if (val === "") return true;
3015
+ const allowedChars = /^[a-zA-Z0-9\-_/]+$/;
3016
+ if (!allowedChars.test(val)) return false;
3017
+ if (/^[-_/]+$/.test(val)) return false;
3018
+ const segments = val.split("/");
3019
+ for (const segment of segments) {
3020
+ if (segment && /^[-_]+$/.test(segment)) {
3021
+ return false;
3022
+ }
3023
+ }
3024
+ return true;
3025
+ },
3026
+ {
3027
+ message: "worktreePrefix contains invalid characters. Only alphanumeric characters, hyphens (-), underscores (_), and forward slashes (/) are allowed. Use forward slashes for nested directories."
3028
+ }
3029
+ ).describe(
3030
+ "Prefix for worktree directories. Empty string disables prefix. Defaults to <repo-name>-looms if not set."
3031
+ ),
3032
+ protectedBranches: z.array(z.string().min(1, "Protected branch name cannot be empty")).optional().describe('List of branches that cannot be deleted (defaults to [mainBranch, "main", "master", "develop"])'),
3033
+ copyGitIgnoredPatterns: z.array(z.string().min(1, "Pattern cannot be empty")).optional().describe(`Glob patterns for gitignored files to copy to looms (e.g., ["*.db", "data/*.sqlite"]). Great for local dbs and large test data files that are too big to commit to git. Note: .env (dotenv-flow) files, iloom's and claude's local settings are automatically copied and do not need to be specified here.`),
3034
+ workflows: WorkflowsSettingsSchemaNoDefaults.describe("Per-workflow-type permission configurations"),
3035
+ agents: z.record(z.string(), AgentSettingsSchema).optional().nullable().describe(
3036
+ 'Per-agent configuration overrides. Available agents: iloom-issue-analyzer (analyzes issues), iloom-issue-planner (creates implementation plans), iloom-issue-analyze-and-plan (combined analysis and planning), iloom-issue-complexity-evaluator (evaluates complexity), iloom-issue-enhancer (enhances issue descriptions), iloom-issue-implementer (implements code changes), iloom-code-reviewer (reviews code changes against requirements), iloom-artifact-reviewer (reviews artifacts before posting), iloom-swarm-worker (swarm worker agent, dynamically generated). The iloom-swarm-worker agent supports a nested "agents" sub-record for configuring phase agent models specifically in swarm mode.'
3037
+ ),
3038
+ spin: z.object({
3039
+ model: z.enum(["sonnet", "opus", "haiku"]).optional()
3040
+ }).optional().describe("Spin orchestrator configuration"),
3041
+ plan: z.object({
3042
+ model: z.enum(["sonnet", "opus", "haiku"]).optional(),
3043
+ planner: z.enum(["claude", "gemini", "codex"]).optional(),
3044
+ reviewer: z.enum(["claude", "gemini", "codex", "none"]).optional()
3045
+ }).optional().describe("Plan command configuration"),
3046
+ summary: z.object({
3047
+ model: z.enum(["sonnet", "opus", "haiku"]).optional()
3048
+ }).optional().describe("Session summary generation configuration"),
3049
+ capabilities: CapabilitiesSettingsSchemaNoDefaults.describe("Project capability configurations"),
3050
+ databaseProviders: DatabaseProvidersSettingsSchema.describe("Database provider configurations"),
3051
+ issueManagement: z.object({
3052
+ provider: z.enum(["github", "linear", "jira"]).optional().describe("Issue tracker provider (github, linear, jira)"),
3053
+ github: z.object({
3054
+ remote: z.string().min(1, "Remote name cannot be empty").describe("Git remote name to use for GitHub operations")
3055
+ }).optional(),
3056
+ linear: z.object({
3057
+ teamId: z.string().min(1, "Team ID cannot be empty").describe('Linear team identifier (e.g., "ENG", "PLAT")'),
3058
+ branchFormat: z.string().optional().describe("Branch naming template for Linear issues"),
3059
+ apiToken: z.string().optional().describe("Linear API token (lin_api_...). SECURITY: Store in settings.local.json only, never commit to source control.")
3060
+ }).optional(),
3061
+ jira: z.object({
3062
+ host: z.string().min(1, "Jira host cannot be empty").describe('Jira instance URL (e.g., "https://yourcompany.atlassian.net")'),
3063
+ username: z.string().min(1, "Jira username/email cannot be empty").describe("Jira username or email address"),
3064
+ apiToken: z.string().optional().describe("Jira API token. SECURITY: Store in settings.local.json only, never commit to source control. Generate at: https://id.atlassian.com/manage-profile/security/api-tokens"),
3065
+ projectKey: z.string().min(1, "Project key cannot be empty").describe('Jira project key (e.g., "PROJ", "ENG")'),
3066
+ boardId: z.string().optional().describe("Jira board ID for sprint/workflow operations (optional)"),
3067
+ transitionMappings: z.record(z.string(), z.string()).optional().describe('Map iloom states to Jira transition names (e.g., {"In Review": "Start Review"})'),
3068
+ defaultIssueType: z.string().min(1).optional().describe('Default Jira issue type name for creating issues (e.g., "Task", "Story", "Bug")'),
3069
+ defaultSubtaskType: z.string().min(1).optional().describe('Default Jira issue type name for creating subtasks/child issues (e.g., "Subtask", "Sub-task")'),
3070
+ doneStatuses: z.array(z.string()).optional().default(["Done"]).describe('Status names to exclude from issue lists (e.g., ["Done", "Closed", "Verify"])')
3071
+ }).optional()
3072
+ }).optional().describe("Issue management configuration"),
3073
+ mergeBehavior: z.object({
3074
+ mode: z.enum(["local", "github-pr", "github-draft-pr"]).optional(),
3075
+ remote: z.string().optional(),
3076
+ autoCommitPush: z.boolean().optional().describe(
3077
+ "Auto-commit and push after code review in draft PR mode. Defaults to true when mode is github-draft-pr."
3078
+ ),
3079
+ openBrowserOnFinish: z.boolean().optional().describe(
3080
+ "Open the PR in the default browser after finishing in github-pr or github-draft-pr mode. Use --no-browser flag to override."
3081
+ )
3082
+ }).optional().describe("Merge behavior configuration: local (merge locally), github-pr (create PR), or github-draft-pr (create draft PR at start, mark ready on finish)"),
3083
+ ide: z.object({
3084
+ type: z.enum(["vscode", "cursor", "webstorm", "sublime", "intellij", "windsurf", "antigravity"]).optional().describe(
3085
+ "IDE to launch when starting a loom. Options: vscode (Visual Studio Code), cursor (Cursor AI editor), webstorm (JetBrains WebStorm), sublime (Sublime Text), intellij (JetBrains IntelliJ IDEA), windsurf (Windsurf editor), antigravity (Antigravity IDE)."
3086
+ )
3087
+ }).optional().describe(
3088
+ "IDE configuration for workspace launches. Controls which editor opens when you start a loom. Supports VSCode, Cursor, WebStorm, Sublime Text, IntelliJ, Windsurf, and Antigravity. Note: Color synchronization (title bar colors) only works with VSCode-compatible editors (vscode, cursor, windsurf, antigravity)."
3089
+ ),
3090
+ colors: z.object({
3091
+ terminal: z.boolean().optional().describe("Apply terminal background colors based on branch name (macOS only)"),
3092
+ vscode: z.boolean().optional().describe(
3093
+ "Apply VSCode/Cursor title bar colors based on branch name. Note: This modifies .vscode/settings.json which may be in source control."
3094
+ )
3095
+ }).optional().describe("Color synchronization settings for workspace identification"),
3096
+ attribution: z.enum(["off", "upstreamOnly", "on"]).optional().describe(
3097
+ 'Controls when iloom attribution appears in session summaries. "off" - never show attribution. "upstreamOnly" - only show for contributions to external repositories (e.g., open source). "on" - always show attribution.'
3098
+ ),
3099
+ git: z.object({
3100
+ commitTimeout: z.number().min(1e3, "Commit timeout must be at least 1000ms").max(6e5, "Commit timeout cannot exceed 600000ms (10 minutes)").optional().describe("Timeout in milliseconds for git commit operations. Increase for long-running pre-commit hooks.")
3101
+ }).optional().describe("Git operation settings")
3102
+ });
3103
+ function redactSensitiveFields(obj) {
3104
+ if (obj === null || obj === void 0) return obj;
3105
+ if (typeof obj !== "object") return obj;
3106
+ if (Array.isArray(obj)) return obj.map(redactSensitiveFields);
3107
+ const sensitiveKeys = ["apitoken", "token", "secret", "password"];
3108
+ const result = {};
3109
+ for (const [key, value] of Object.entries(obj)) {
3110
+ const lowerKey = key.toLowerCase();
3111
+ if (sensitiveKeys.some((s) => lowerKey.includes(s)) && typeof value === "string") {
3112
+ result[key] = "[REDACTED]";
3113
+ } else if (typeof value === "object" && value !== null) {
3114
+ result[key] = redactSensitiveFields(value);
3115
+ } else {
3116
+ result[key] = value;
3117
+ }
3118
+ }
3119
+ return result;
3120
+ }
3121
+ var SettingsManager = class {
3122
+ /**
3123
+ * Load settings from global, project, and local sources with proper precedence
3124
+ * Merge hierarchy (lowest to highest priority):
3125
+ * 1. Global settings (~/.config/iloom-ai/settings.json)
3126
+ * 2. Project settings (<PROJECT_ROOT>/.iloom/settings.json)
3127
+ * 3. Local settings (<PROJECT_ROOT>/.iloom/settings.local.json)
3128
+ * 4. CLI overrides (--set flags)
3129
+ * Returns empty object if all files don't exist (not an error)
3130
+ */
3131
+ async loadSettings(projectRoot, cliOverrides) {
3132
+ const root = this.getProjectRoot(projectRoot);
3133
+ const globalSettings = await this.loadGlobalSettingsFile();
3134
+ const globalSettingsPath = this.getGlobalSettingsPath();
3135
+ logger.debug(`\u{1F30D} Global settings from ${globalSettingsPath}:`, JSON.stringify(redactSensitiveFields(globalSettings), null, 2));
3136
+ const baseSettings = await this.loadSettingsFile(root, "settings.json");
3137
+ const baseSettingsPath = path2.join(root, ".iloom", "settings.json");
3138
+ logger.debug(`\u{1F4C4} Base settings from ${baseSettingsPath}:`, JSON.stringify(redactSensitiveFields(baseSettings), null, 2));
3139
+ const localSettings = await this.loadSettingsFile(root, "settings.local.json");
3140
+ const localSettingsPath = path2.join(root, ".iloom", "settings.local.json");
3141
+ logger.debug(`\u{1F4C4} Local settings from ${localSettingsPath}:`, JSON.stringify(redactSensitiveFields(localSettings), null, 2));
3142
+ let merged = this.mergeSettings(this.mergeSettings(globalSettings, baseSettings), localSettings);
3143
+ logger.debug("\u{1F504} After merging global + base + local settings:", JSON.stringify(redactSensitiveFields(merged), null, 2));
3144
+ if (cliOverrides && Object.keys(cliOverrides).length > 0) {
3145
+ logger.debug("\u2699\uFE0F CLI overrides to apply:", JSON.stringify(redactSensitiveFields(cliOverrides), null, 2));
3146
+ merged = this.mergeSettings(merged, cliOverrides);
3147
+ logger.debug("\u{1F504} After applying CLI overrides:", JSON.stringify(redactSensitiveFields(merged), null, 2));
3148
+ }
3149
+ try {
3150
+ const finalSettings = IloomSettingsSchema.parse(merged);
3151
+ this.logFinalConfiguration(finalSettings);
3152
+ return finalSettings;
3153
+ } catch (error) {
3154
+ if (error instanceof z.ZodError) {
3155
+ const errorMsg = this.formatAllZodErrors(error, "<merged settings>");
3156
+ if (cliOverrides && Object.keys(cliOverrides).length > 0) {
3157
+ throw new Error(`${errorMsg.message}
3158
+
3159
+ Note: CLI overrides were applied. Check your --set arguments.`);
3160
+ }
3161
+ throw errorMsg;
3162
+ }
3163
+ throw error;
3164
+ }
3165
+ }
3166
+ /**
3167
+ * Log the final merged configuration for debugging
3168
+ */
3169
+ logFinalConfiguration(settings2) {
3170
+ logger.debug("\u{1F4CB} Final merged configuration:", JSON.stringify(redactSensitiveFields(settings2), null, 2));
3171
+ }
3172
+ /**
3173
+ * Load and parse a single settings file
3174
+ * Returns empty object if file doesn't exist (not an error)
3175
+ * Uses non-defaulting schema to prevent polluting partial settings with defaults before merge
3176
+ */
3177
+ async loadSettingsFile(projectRoot, filename) {
3178
+ const settingsPath = path2.join(projectRoot, ".iloom", filename);
3179
+ try {
3180
+ const content = await readFile(settingsPath, "utf-8");
3181
+ let parsed;
3182
+ try {
3183
+ parsed = JSON.parse(content);
3184
+ } catch (error) {
3185
+ throw new Error(
3186
+ `Failed to parse settings file at ${settingsPath}: ${error instanceof Error ? error.message : "Invalid JSON"}`
3187
+ );
3188
+ }
3189
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
3190
+ throw new Error(
3191
+ `Settings validation failed at ${filename}:
3192
+ - root: Expected object, received ${typeof parsed}`
3193
+ );
3194
+ }
3195
+ return parsed;
3196
+ } catch (error) {
3197
+ if (error.code === "ENOENT") {
3198
+ logger.debug(`No settings file found at ${settingsPath}, using defaults`);
3199
+ return {};
3200
+ }
3201
+ throw error;
3202
+ }
3203
+ }
3204
+ /**
3205
+ * Deep merge two settings objects with priority to override
3206
+ * Uses deepmerge library with array replacement strategy
3207
+ */
3208
+ mergeSettings(base, override) {
3209
+ return deepmerge(base, override, {
3210
+ // Replace arrays instead of concatenating them
3211
+ arrayMerge: (_destinationArray, sourceArray) => sourceArray
3212
+ });
3213
+ }
3214
+ /**
3215
+ * Format all Zod validation errors into a single error message
3216
+ */
3217
+ formatAllZodErrors(error, settingsPath) {
3218
+ const errorMessages = error.issues.map((issue) => {
3219
+ const path3 = issue.path.length > 0 ? issue.path.join(".") : "root";
3220
+ return ` - ${path3}: ${issue.message}`;
3221
+ });
3222
+ return new Error(
3223
+ `Settings validation failed at ${settingsPath}:
3224
+ ${errorMessages.join("\n")}`
3225
+ );
3226
+ }
3227
+ /**
3228
+ * Validate settings structure and model names using Zod schema
3229
+ * This method is kept for testing purposes but uses Zod internally
3230
+ * @internal - Only used in tests via bracket notation
3231
+ */
3232
+ // @ts-expect-error - Used in tests via bracket notation, TypeScript can't detect this usage
3233
+ validateSettings(settings2) {
3234
+ try {
3235
+ IloomSettingsSchema.parse(settings2);
3236
+ } catch (error) {
3237
+ if (error instanceof z.ZodError) {
3238
+ throw this.formatAllZodErrors(error, "<validation>");
3239
+ }
3240
+ throw error;
3241
+ }
3242
+ }
3243
+ /**
3244
+ * Get project root (defaults to process.cwd())
3245
+ */
3246
+ getProjectRoot(projectRoot) {
3247
+ return projectRoot ?? process.cwd();
3248
+ }
3249
+ /**
3250
+ * Get global config directory path (~/.config/iloom-ai)
3251
+ */
3252
+ getGlobalConfigDir() {
3253
+ return path2.join(os2.homedir(), ".config", "iloom-ai");
3254
+ }
3255
+ /**
3256
+ * Get global settings file path (~/.config/iloom-ai/settings.json)
3257
+ */
3258
+ getGlobalSettingsPath() {
3259
+ return path2.join(this.getGlobalConfigDir(), "settings.json");
3260
+ }
3261
+ /**
3262
+ * Load and parse global settings file
3263
+ * Returns empty object if file doesn't exist (not an error)
3264
+ * Warns but returns empty object on validation/parse errors (graceful degradation)
3265
+ */
3266
+ async loadGlobalSettingsFile() {
3267
+ const settingsPath = this.getGlobalSettingsPath();
3268
+ try {
3269
+ const content = await readFile(settingsPath, "utf-8");
3270
+ let parsed;
3271
+ try {
3272
+ parsed = JSON.parse(content);
3273
+ } catch (error) {
3274
+ logger.warn(
3275
+ `Failed to parse global settings file at ${settingsPath}: ${error instanceof Error ? error.message : "Invalid JSON"}. Ignoring global settings.`
3276
+ );
3277
+ return {};
3278
+ }
3279
+ try {
3280
+ const validated = IloomSettingsSchemaNoDefaults.strict().parse(parsed);
3281
+ return validated;
3282
+ } catch (error) {
3283
+ if (error instanceof z.ZodError) {
3284
+ const errorMsg = this.formatAllZodErrors(error, "global settings");
3285
+ logger.warn(`${errorMsg.message}. Ignoring global settings.`);
3286
+ } else {
3287
+ logger.warn(`Validation error in global settings: ${error instanceof Error ? error.message : "Unknown error"}. Ignoring global settings.`);
3288
+ }
3289
+ return {};
3290
+ }
3291
+ } catch (error) {
3292
+ if (error.code === "ENOENT") {
3293
+ logger.debug(`No global settings file found at ${settingsPath}`);
3294
+ return {};
3295
+ }
3296
+ logger.warn(`Error reading global settings file at ${settingsPath}: ${error instanceof Error ? error.message : "Unknown error"}. Ignoring global settings.`);
3297
+ return {};
3298
+ }
3299
+ }
3300
+ /**
3301
+ * Get effective protected branches list with mainBranch always included
3302
+ *
3303
+ * This method provides a single source of truth for protected branches logic:
3304
+ * 1. Use configured protectedBranches if provided
3305
+ * 2. Otherwise use defaults: [mainBranch, 'main', 'master', 'develop']
3306
+ * 3. ALWAYS ensure mainBranch is included even if user configured custom list
3307
+ *
3308
+ * @param projectRoot - Optional project root directory (defaults to process.cwd())
3309
+ * @returns Array of protected branch names with mainBranch guaranteed to be included
3310
+ */
3311
+ async getProtectedBranches(projectRoot) {
3312
+ const settings2 = await this.loadSettings(projectRoot);
3313
+ const mainBranch = settings2.mainBranch ?? "main";
3314
+ let protectedBranches;
3315
+ if (settings2.protectedBranches) {
3316
+ protectedBranches = settings2.protectedBranches.includes(mainBranch) ? settings2.protectedBranches : [mainBranch, ...settings2.protectedBranches];
3317
+ } else {
3318
+ protectedBranches = [mainBranch, "main", "master", "develop"];
3319
+ }
3320
+ return protectedBranches;
3321
+ }
3322
+ /**
3323
+ * Get the spin orchestrator model with default applied
3324
+ * Default is defined in SpinAgentSettingsSchema
3325
+ *
3326
+ * @param settings - Pre-loaded settings object
3327
+ * @returns Model shorthand ('opus', 'sonnet', or 'haiku')
3328
+ */
3329
+ getSpinModel(settings2) {
3330
+ var _a;
3331
+ return ((_a = settings2 == null ? void 0 : settings2.spin) == null ? void 0 : _a.model) ?? SpinAgentSettingsSchema.parse({}).model;
3332
+ }
3333
+ /**
3334
+ * Get the plan command model with default applied
3335
+ * Default is defined in PlanCommandSettingsSchema
3336
+ *
3337
+ * @param settings - Pre-loaded settings object
3338
+ * @returns Model shorthand ('opus', 'sonnet', or 'haiku')
3339
+ */
3340
+ getPlanModel(settings2) {
3341
+ var _a;
3342
+ return ((_a = settings2 == null ? void 0 : settings2.plan) == null ? void 0 : _a.model) ?? PlanCommandSettingsSchema.parse({}).model;
3343
+ }
3344
+ /**
3345
+ * Get the plan command planner with default applied
3346
+ * Default is 'claude'
3347
+ *
3348
+ * @param settings - Pre-loaded settings object
3349
+ * @returns Planner provider ('claude', 'gemini', or 'codex')
3350
+ */
3351
+ getPlanPlanner(settings2) {
3352
+ var _a;
3353
+ return ((_a = settings2 == null ? void 0 : settings2.plan) == null ? void 0 : _a.planner) ?? "claude";
3354
+ }
3355
+ /**
3356
+ * Get the plan command reviewer with default applied
3357
+ * Default is 'none' (no review step)
3358
+ *
3359
+ * @param settings - Pre-loaded settings object
3360
+ * @returns Reviewer provider ('claude', 'gemini', 'codex', or 'none')
3361
+ */
3362
+ getPlanReviewer(settings2) {
3363
+ var _a;
3364
+ return ((_a = settings2 == null ? void 0 : settings2.plan) == null ? void 0 : _a.reviewer) ?? "none";
3365
+ }
3366
+ /**
3367
+ * Get the session summary model with default applied
3368
+ * Default is defined in SummarySettingsSchema
3369
+ *
3370
+ * @param settings - Pre-loaded settings object
3371
+ * @returns Model shorthand ('opus', 'sonnet', or 'haiku')
3372
+ */
3373
+ getSummaryModel(settings2) {
3374
+ var _a;
3375
+ return ((_a = settings2 == null ? void 0 : settings2.summary) == null ? void 0 : _a.model) ?? SummarySettingsSchema.parse({}).model;
3376
+ }
3377
+ };
3378
+
3379
+ // src/mcp/JiraIssueManagementProvider.ts
3380
+ function normalizeAuthor2(author) {
3381
+ if (!author) return null;
3382
+ return {
3383
+ id: author.accountId ?? author.emailAddress ?? "unknown",
3384
+ displayName: author.displayName ?? author.emailAddress ?? "Unknown",
3385
+ ...author.emailAddress && { email: author.emailAddress },
3386
+ ...author.accountId && { accountId: author.accountId }
3387
+ };
3388
+ }
3389
+ var getJiraTrackerConfig = (settings2) => {
3390
+ var _a;
3391
+ const jiraSettings = (_a = settings2.issueManagement) == null ? void 0 : _a.jira;
3392
+ if ((jiraSettings == null ? void 0 : jiraSettings.host) && (jiraSettings == null ? void 0 : jiraSettings.username) && (jiraSettings == null ? void 0 : jiraSettings.apiToken) && (jiraSettings == null ? void 0 : jiraSettings.projectKey)) {
3393
+ const config = {
3394
+ host: jiraSettings.host,
3395
+ username: jiraSettings.username,
3396
+ apiToken: jiraSettings.apiToken,
3397
+ projectKey: jiraSettings.projectKey
3398
+ };
3399
+ if (jiraSettings.transitionMappings) {
3400
+ config.transitionMappings = jiraSettings.transitionMappings;
3401
+ }
3402
+ if (jiraSettings.defaultIssueType) {
3403
+ config.defaultIssueType = jiraSettings.defaultIssueType;
3404
+ }
3405
+ if (jiraSettings.defaultSubtaskType) {
3406
+ config.defaultSubtaskType = jiraSettings.defaultSubtaskType;
3407
+ }
3408
+ return config;
3409
+ }
3410
+ if (process.env.JIRA_HOST && process.env.JIRA_USERNAME && process.env.JIRA_API_TOKEN && process.env.JIRA_PROJECT_KEY) {
3411
+ const config = {
3412
+ host: process.env.JIRA_HOST,
3413
+ username: process.env.JIRA_USERNAME,
3414
+ apiToken: process.env.JIRA_API_TOKEN,
3415
+ projectKey: process.env.JIRA_PROJECT_KEY
3416
+ };
3417
+ if (process.env.JIRA_TRANSITION_MAPPINGS) {
3418
+ try {
3419
+ config.transitionMappings = JSON.parse(process.env.JIRA_TRANSITION_MAPPINGS);
3420
+ } catch {
3421
+ throw new Error("Invalid JSON in JIRA_TRANSITION_MAPPINGS environment variable");
3422
+ }
3423
+ }
3424
+ if (process.env.JIRA_DEFAULT_ISSUE_TYPE) {
3425
+ config.defaultIssueType = process.env.JIRA_DEFAULT_ISSUE_TYPE;
3426
+ }
3427
+ if (process.env.JIRA_DEFAULT_SUBTASK_TYPE) {
3428
+ config.defaultSubtaskType = process.env.JIRA_DEFAULT_SUBTASK_TYPE;
3429
+ }
3430
+ return config;
3431
+ }
3432
+ throw new Error(
3433
+ "Missing required Jira settings: issueManagement.jira.{host, username, apiToken, projectKey} or corresponding environment variables"
3434
+ );
3435
+ };
3436
+ var JiraIssueManagementProvider = class _JiraIssueManagementProvider {
3437
+ constructor(settings2) {
3438
+ this.providerName = "jira";
3439
+ this.issuePrefix = "";
3440
+ const config = getJiraTrackerConfig(settings2);
3441
+ this.tracker = new JiraIssueTracker(config);
3442
+ this.projectKey = config.projectKey;
3443
+ }
3444
+ /**
3445
+ * Static factory for convenience when settings aren't pre-loaded
3446
+ */
3447
+ static async create() {
3448
+ const settingsManager = new SettingsManager();
3449
+ const settings2 = await settingsManager.loadSettings();
3450
+ return new _JiraIssueManagementProvider(settings2);
3451
+ }
3452
+ /**
3453
+ * Fetch issue details using JiraIssueTracker
3454
+ */
3455
+ async getIssue(input) {
3456
+ const { number, includeComments = true } = input;
3457
+ const issue = await this.tracker.getIssue(number);
3458
+ const issueExt = issue;
3459
+ const result = {
3460
+ id: issueExt.id ?? String(issue.number),
3461
+ title: issue.title,
3462
+ body: issue.body,
3463
+ state: issue.state,
3464
+ url: issue.url,
3465
+ provider: "jira",
3466
+ author: normalizeAuthor2(issueExt.author),
3467
+ number: issue.number,
3468
+ key: issueExt.key,
3469
+ // Preserve Jira-specific fields
3470
+ ...issueExt.issueType && { issueType: issueExt.issueType },
3471
+ ...issueExt.priority && { priority: issueExt.priority },
3472
+ ...issueExt.status && { status: issueExt.status }
3473
+ };
3474
+ if (issue.labels && issue.labels.length > 0) {
3475
+ result.labels = issue.labels.map((label) => ({ name: label }));
3476
+ }
3477
+ if (issue.assignees && issue.assignees.length > 0) {
3478
+ result.assignees = issue.assignees.map((name) => ({
3479
+ id: name,
3480
+ displayName: name
3481
+ }));
3482
+ }
3483
+ if (includeComments) {
3484
+ const comments = await this.tracker.getComments(number);
3485
+ result.comments = comments.map((comment) => ({
3486
+ id: comment.id,
3487
+ body: comment.body,
3488
+ author: normalizeAuthor2(comment.author),
3489
+ createdAt: comment.createdAt,
3490
+ updatedAt: comment.updatedAt
3491
+ }));
3492
+ }
3493
+ return result;
3494
+ }
3495
+ /**
3496
+ * Fetch a specific comment by ID
3497
+ */
3498
+ async getComment(input) {
3499
+ const { commentId, number } = input;
3500
+ const comments = await this.tracker.getComments(number);
3501
+ const comment = comments.find((c) => c.id === commentId);
3502
+ if (!comment) {
3503
+ throw new Error(`Comment ${commentId} not found on issue ${number}`);
3504
+ }
3505
+ return {
3506
+ id: comment.id,
3507
+ body: comment.body,
3508
+ author: normalizeAuthor2(comment.author),
3509
+ created_at: comment.createdAt,
3510
+ updated_at: comment.updatedAt
3511
+ };
3512
+ }
3513
+ /**
3514
+ * Create a new comment on an issue
3515
+ */
3516
+ async createComment(input) {
3517
+ const { number, body } = input;
3518
+ const normalizedKey = this.tracker.normalizeIdentifier(number);
3519
+ const comment = await this.tracker.addComment(normalizedKey, body);
3520
+ return {
3521
+ id: comment.id,
3522
+ url: `${this.tracker.getConfig().host}/browse/${normalizedKey}?focusedCommentId=${comment.id}`,
3523
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
3524
+ };
3525
+ }
3526
+ /**
3527
+ * Update an existing comment
3528
+ */
3529
+ async updateComment(input) {
3530
+ const { commentId, number, body } = input;
3531
+ const normalizedKey = this.tracker.normalizeIdentifier(number);
3532
+ await this.tracker.updateComment(normalizedKey, commentId, body);
3533
+ return {
3534
+ id: commentId,
3535
+ url: `${this.tracker.getConfig().host}/browse/${normalizedKey}?focusedCommentId=${commentId}`,
3536
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
3537
+ };
3538
+ }
3539
+ /**
3540
+ * Create a new issue
3541
+ */
3542
+ async createIssue(input) {
3543
+ const { title, body } = input;
3544
+ const issue = await this.tracker.createIssue(title, body);
3545
+ const result = {
3546
+ id: String(issue.number),
3547
+ url: issue.url
3548
+ };
3549
+ if (typeof issue.number === "number") {
3550
+ result.number = issue.number;
3551
+ }
3552
+ return result;
3553
+ }
3554
+ /**
3555
+ * Fetch pull request details
3556
+ * Jira does not have pull requests - throw like Linear does
3557
+ */
3558
+ async getPR(_input) {
3559
+ throw new Error(
3560
+ "Jira does not support pull requests. PRs exist only on GitHub. Use the GitHub provider for PR operations."
3561
+ );
3562
+ }
3563
+ /**
3564
+ * Create a child issue linked to a parent issue
3565
+ * Uses Jira's parent field to create a subtask
3566
+ */
3567
+ async createChildIssue(input) {
3568
+ const { parentId, title, body } = input;
3569
+ const parentKey = this.tracker.normalizeIdentifier(parentId);
3570
+ const jiraIssue = await this.tracker.getApiClient().createIssueWithParent(
3571
+ this.projectKey,
3572
+ title,
3573
+ body,
3574
+ parentKey,
3575
+ this.tracker.getConfig().defaultSubtaskType
3576
+ );
3577
+ return {
3578
+ id: jiraIssue.key,
3579
+ url: `${this.tracker.getConfig().host}/browse/${jiraIssue.key}`
3580
+ };
3581
+ }
3582
+ /**
3583
+ * Create a blocking dependency between two issues
3584
+ * Uses Jira issue links with "Blocks" link type
3585
+ */
3586
+ async createDependency(input) {
3587
+ const blockingKey = this.tracker.normalizeIdentifier(input.blockingIssue);
3588
+ const blockedKey = this.tracker.normalizeIdentifier(input.blockedIssue);
3589
+ await this.tracker.getApiClient().createIssueLink(blockingKey, blockedKey, "Blocks");
3590
+ }
3591
+ /**
3592
+ * Get dependencies for an issue
3593
+ * Parses issue links of type "Blocks"
3594
+ */
3595
+ async getDependencies(input) {
3596
+ const issueKey = this.tracker.normalizeIdentifier(input.number);
3597
+ const host = this.tracker.getConfig().host;
3598
+ const issue = await this.tracker.getApiClient().getIssue(issueKey);
3599
+ const links = issue.fields.issuelinks ?? [];
3600
+ const blocking = [];
3601
+ const blockedBy = [];
3602
+ for (const link of links) {
3603
+ if (link.type.name !== "Blocks") continue;
3604
+ if (link.inwardIssue) {
3605
+ blockedBy.push({
3606
+ id: link.inwardIssue.key,
3607
+ title: link.inwardIssue.fields.summary,
3608
+ url: `${host}/browse/${link.inwardIssue.key}`,
3609
+ state: link.inwardIssue.fields.status.name.toLowerCase()
3610
+ });
3611
+ }
3612
+ if (link.outwardIssue) {
3613
+ blocking.push({
3614
+ id: link.outwardIssue.key,
3615
+ title: link.outwardIssue.fields.summary,
3616
+ url: `${host}/browse/${link.outwardIssue.key}`,
3617
+ state: link.outwardIssue.fields.status.name.toLowerCase()
3618
+ });
3619
+ }
3620
+ }
3621
+ if (input.direction === "blocking") {
3622
+ return { blocking, blockedBy: [] };
3623
+ }
3624
+ if (input.direction === "blocked_by") {
3625
+ return { blocking: [], blockedBy };
3626
+ }
3627
+ return { blocking, blockedBy };
3628
+ }
3629
+ /**
3630
+ * Remove a blocking dependency between two issues
3631
+ * Finds the matching "Blocks" link and deletes it
3632
+ */
3633
+ async removeDependency(input) {
3634
+ const blockingKey = this.tracker.normalizeIdentifier(input.blockingIssue);
3635
+ const blockedKey = this.tracker.normalizeIdentifier(input.blockedIssue);
3636
+ const issue = await this.tracker.getApiClient().getIssue(blockedKey);
3637
+ const links = issue.fields.issuelinks ?? [];
3638
+ const matchingLink = links.find(
3639
+ (link) => {
3640
+ var _a;
3641
+ return link.type.name === "Blocks" && ((_a = link.inwardIssue) == null ? void 0 : _a.key) === blockingKey;
3642
+ }
3643
+ );
3644
+ if (!matchingLink) {
3645
+ throw new Error(
3646
+ `No "Blocks" dependency found from ${blockingKey} to ${blockedKey}`
3647
+ );
3648
+ }
3649
+ await this.tracker.getApiClient().deleteIssueLink(matchingLink.id);
3650
+ }
3651
+ /**
3652
+ * Get child issues of a parent issue
3653
+ * Uses JQL search: parent = KEY
3654
+ */
3655
+ async getChildIssues(input) {
3656
+ const parentKey = this.tracker.normalizeIdentifier(input.number);
3657
+ const host = this.tracker.getConfig().host;
3658
+ const issues = await this.tracker.getApiClient().searchIssues(`parent = "${escapeJql(parentKey)}"`);
3659
+ return issues.map((issue) => ({
3660
+ id: issue.key,
3661
+ title: issue.fields.summary,
3662
+ url: `${host}/browse/${issue.key}`,
3663
+ state: issue.fields.status.name.toLowerCase()
3664
+ }));
3665
+ }
3666
+ /**
3667
+ * Close an issue by transitioning to "Done" state
3668
+ */
3669
+ async closeIssue(input) {
3670
+ const issueKey = this.tracker.normalizeIdentifier(input.number);
3671
+ await this.tracker.closeIssue(issueKey);
3672
+ }
3673
+ /**
3674
+ * Reopen a closed issue
3675
+ */
3676
+ async reopenIssue(input) {
3677
+ const issueKey = this.tracker.normalizeIdentifier(input.number);
3678
+ await this.tracker.reopenIssue(issueKey);
3679
+ }
3680
+ /**
3681
+ * Edit an issue's properties
3682
+ * State changes are delegated to closeIssue/reopenIssue
3683
+ */
3684
+ async editIssue(input) {
3685
+ const { number, title, body, state } = input;
3686
+ if (state === "closed") {
3687
+ await this.closeIssue({ number });
3688
+ } else if (state === "open") {
3689
+ await this.reopenIssue({ number });
3690
+ }
3691
+ if (title !== void 0 || body !== void 0) {
3692
+ const issueKey = this.tracker.normalizeIdentifier(number);
3693
+ await this.tracker.getApiClient().updateIssue(issueKey, {
3694
+ ...title !== void 0 && { summary: title },
3695
+ ...body !== void 0 && { description: body }
3696
+ });
3697
+ }
3698
+ }
3699
+ };
3700
+
3701
+ // src/mcp/IssueManagementProviderFactory.ts
3702
+ var IssueManagementProviderFactory = class {
3703
+ /**
3704
+ * Create an issue management provider based on the provider type
3705
+ * @param provider - The provider type (github, linear, jira)
3706
+ * @param settings - Required for Jira provider, optional for others
3707
+ */
3708
+ static create(provider, settings2) {
3709
+ switch (provider) {
3710
+ case "github":
3711
+ return new GitHubIssueManagementProvider();
3712
+ case "linear":
3713
+ return new LinearIssueManagementProvider();
3714
+ case "jira":
3715
+ if (!settings2) {
3716
+ throw new Error("Settings required for Jira provider");
3717
+ }
3718
+ return new JiraIssueManagementProvider(settings2);
3719
+ default:
3720
+ throw new Error(`Unsupported issue management provider: ${provider}`);
3721
+ }
3722
+ }
3723
+ };
3724
+
3725
+ // src/mcp/issue-management-server.ts
3726
+ var settings;
3727
+ function validateEnvironment() {
3728
+ const provider = process.env.ISSUE_PROVIDER;
3729
+ if (!provider) {
3730
+ console.error("Missing required environment variable: ISSUE_PROVIDER");
3731
+ process.exit(1);
3732
+ }
3733
+ if (provider !== "github" && provider !== "linear" && provider !== "jira") {
3734
+ console.error(`Invalid ISSUE_PROVIDER: ${provider}. Must be 'github', 'linear', or 'jira'`);
3735
+ process.exit(1);
3736
+ }
3737
+ if (provider === "github") {
3738
+ const required = ["REPO_OWNER", "REPO_NAME"];
3739
+ const missing = required.filter((key) => !process.env[key]);
3740
+ if (missing.length > 0) {
3741
+ console.error(
3742
+ `Missing required environment variables for GitHub provider: ${missing.join(", ")}`
3743
+ );
3744
+ process.exit(1);
3745
+ }
3746
+ }
3747
+ if (provider === "linear") {
3748
+ if (!process.env.LINEAR_API_TOKEN) {
3749
+ console.error("Missing required environment variable for Linear provider: LINEAR_API_TOKEN");
3750
+ process.exit(1);
3751
+ }
3752
+ }
3753
+ if (provider === "jira") {
3754
+ const required = ["JIRA_HOST", "JIRA_USERNAME", "JIRA_API_TOKEN", "JIRA_PROJECT_KEY"];
3755
+ const missing = required.filter((key) => !process.env[key]);
3756
+ if (missing.length > 0) {
3757
+ console.error(
3758
+ `Missing required environment variables for Jira provider: ${missing.join(", ")}`
3759
+ );
3760
+ process.exit(1);
3761
+ }
3762
+ }
3763
+ return provider;
3764
+ }
3765
+ var server = new McpServer({
3766
+ name: "issue-management-broker",
3767
+ version: "0.1.0"
3768
+ });
3769
+ var flexibleAuthorSchema = z2.object({
3770
+ id: z2.string(),
3771
+ displayName: z2.string()
3772
+ }).passthrough();
3773
+ server.registerTool(
3774
+ "get_issue",
3775
+ {
3776
+ title: "Get Issue",
3777
+ description: "Fetch issue details including body, title, comments, labels, assignees, and other metadata. Author fields vary by provider: GitHub uses { login }, Linear uses { name, displayName }, Jira uses { displayName, accountId }. All authors have normalized core fields: { id, displayName } plus provider-specific fields.",
3778
+ inputSchema: {
3779
+ number: z2.string().describe("The issue identifier"),
3780
+ includeComments: z2.boolean().optional().describe("Whether to include comments (default: true)"),
3781
+ repo: z2.string().optional().describe(
3782
+ 'Optional repository in "owner/repo" format or full GitHub URL. When not provided, uses the current repository. GitHub only.'
3783
+ )
3784
+ },
3785
+ outputSchema: {
3786
+ // Core validated fields
3787
+ id: z2.string().describe("Issue identifier"),
3788
+ title: z2.string().describe("Issue title"),
3789
+ body: z2.string().describe("Issue body/description"),
3790
+ state: z2.string().describe("Issue state (open, closed, etc.)"),
3791
+ url: z2.string().describe("Issue URL"),
3792
+ provider: z2.enum(["github", "linear", "jira"]).describe("Issue management provider"),
3793
+ // Flexible author - core fields + passthrough
3794
+ author: flexibleAuthorSchema.nullable().describe(
3795
+ "Issue author with normalized { id, displayName } plus provider-specific fields"
3796
+ ),
3797
+ // Optional flexible arrays
3798
+ assignees: z2.array(flexibleAuthorSchema).optional().describe(
3799
+ "Issue assignees with normalized { id, displayName } plus provider-specific fields"
3800
+ ),
3801
+ labels: z2.array(
3802
+ z2.object({ name: z2.string() }).passthrough()
3803
+ ).optional().describe("Issue labels"),
3804
+ // Comments with flexible author
3805
+ comments: z2.array(
3806
+ z2.object({
3807
+ id: z2.string(),
3808
+ body: z2.string(),
3809
+ author: flexibleAuthorSchema.nullable(),
3810
+ createdAt: z2.string()
3811
+ }).passthrough()
3812
+ ).optional().describe("Issue comments with flexible author structure")
3813
+ }
3814
+ },
3815
+ async ({ number, includeComments, repo }) => {
3816
+ console.error(`Fetching issue ${number}${repo ? ` from ${repo}` : ""}`);
3817
+ try {
3818
+ const provider = IssueManagementProviderFactory.create(
3819
+ process.env.ISSUE_PROVIDER,
3820
+ settings
3821
+ );
3822
+ const result = await provider.getIssue({ number, includeComments, repo });
3823
+ console.error(`Issue fetched successfully: ${result.number} - ${result.title}`);
3824
+ return {
3825
+ content: [
3826
+ {
3827
+ type: "text",
3828
+ text: JSON.stringify(result)
3829
+ }
3830
+ ],
3831
+ structuredContent: result
3832
+ };
3833
+ } catch (error) {
3834
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
3835
+ console.error(`Failed to fetch issue: ${errorMessage}`);
3836
+ throw new Error(`Failed to fetch issue: ${errorMessage}`);
3837
+ }
3838
+ }
3839
+ );
3840
+ server.registerTool(
3841
+ "get_pr",
3842
+ {
3843
+ title: "Get Pull Request",
3844
+ description: "Fetch pull request details including title, body, comments, files, commits, and branch information. PRs only exist on GitHub, so this tool always uses GitHub regardless of configured issue tracker. Author fields have normalized core fields: { id, displayName } plus provider-specific fields.",
3845
+ inputSchema: {
3846
+ number: z2.string().describe("The PR number"),
3847
+ includeComments: z2.boolean().optional().describe("Whether to include comments (default: true)"),
3848
+ repo: z2.string().optional().describe(
3849
+ 'Optional repository in "owner/repo" format or full GitHub URL. When not provided, uses the current repository.'
3850
+ )
3851
+ },
3852
+ outputSchema: {
3853
+ // Core validated fields
3854
+ id: z2.string().describe("PR identifier"),
3855
+ number: z2.number().describe("PR number"),
3856
+ title: z2.string().describe("PR title"),
3857
+ body: z2.string().describe("PR body/description"),
3858
+ state: z2.string().describe("PR state (OPEN, CLOSED, MERGED)"),
3859
+ url: z2.string().describe("PR URL"),
3860
+ // Branch info
3861
+ headRefName: z2.string().describe("Source branch name"),
3862
+ baseRefName: z2.string().describe("Target branch name"),
3863
+ // Flexible author - core fields + passthrough
3864
+ author: flexibleAuthorSchema.nullable().describe(
3865
+ "PR author with normalized { id, displayName } plus provider-specific fields"
3866
+ ),
3867
+ // Optional flexible arrays
3868
+ files: z2.array(
3869
+ z2.object({
3870
+ path: z2.string(),
3871
+ additions: z2.number(),
3872
+ deletions: z2.number()
3873
+ }).passthrough()
3874
+ ).optional().describe("Changed files in the PR"),
3875
+ commits: z2.array(
3876
+ z2.object({
3877
+ oid: z2.string(),
3878
+ messageHeadline: z2.string(),
3879
+ author: flexibleAuthorSchema.nullable()
3880
+ }).passthrough()
3881
+ ).optional().describe("Commits in the PR"),
3882
+ comments: z2.array(
3883
+ z2.object({
3884
+ id: z2.string(),
3885
+ body: z2.string(),
3886
+ author: flexibleAuthorSchema.nullable(),
3887
+ createdAt: z2.string()
3888
+ }).passthrough()
3889
+ ).optional().describe("PR comments")
3890
+ }
3891
+ },
3892
+ async ({ number, includeComments, repo }) => {
3893
+ console.error(`Fetching PR ${number}${repo ? ` from ${repo}` : ""}`);
3894
+ try {
3895
+ const provider = new GitHubIssueManagementProvider();
3896
+ const result = await provider.getPR({ number, includeComments, repo });
3897
+ console.error(`PR fetched successfully: #${result.number} - ${result.title}`);
3898
+ return {
3899
+ content: [
3900
+ {
3901
+ type: "text",
3902
+ text: JSON.stringify(result)
3903
+ }
3904
+ ],
3905
+ structuredContent: result
3906
+ };
3907
+ } catch (error) {
3908
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
3909
+ console.error(`Failed to fetch PR: ${errorMessage}`);
3910
+ throw new Error(`Failed to fetch PR: ${errorMessage}`);
3911
+ }
3912
+ }
3913
+ );
3914
+ server.registerTool(
3915
+ "get_review_comments",
3916
+ {
3917
+ title: "Get PR Review Comments",
3918
+ description: "Fetch inline code review comments on a pull request (comments on specific files and lines). Returns comments with file path, line number, diff side, author, and reply threading. Optionally filter by review ID. PRs only exist on GitHub, so this tool always uses GitHub.",
3919
+ inputSchema: {
3920
+ number: z2.string().describe("The PR number"),
3921
+ reviewId: z2.string().optional().describe("Optional review ID to filter comments by a specific review"),
3922
+ repo: z2.string().optional().describe(
3923
+ 'Optional repository in "owner/repo" format or full GitHub URL. When not provided, uses the current repository.'
3924
+ )
3925
+ },
3926
+ outputSchema: {
3927
+ comments: z2.array(
3928
+ z2.object({
3929
+ id: z2.string().describe("Review comment ID"),
3930
+ body: z2.string().describe("Comment body content"),
3931
+ path: z2.string().describe("File path the comment is on"),
3932
+ line: z2.number().nullable().describe("Line number in the diff"),
3933
+ side: z2.string().nullable().describe("Side of the diff (LEFT or RIGHT)"),
3934
+ author: flexibleAuthorSchema.nullable().describe("Comment author"),
3935
+ createdAt: z2.string().describe("Comment creation timestamp"),
3936
+ updatedAt: z2.string().nullable().describe("Comment last updated timestamp"),
3937
+ inReplyToId: z2.string().nullable().describe("ID of the comment this replies to"),
3938
+ pullRequestReviewId: z2.number().nullable().describe("The review this comment belongs to")
3939
+ })
3940
+ ).describe("Inline review comments on the PR")
3941
+ }
3942
+ },
3943
+ async ({ number, reviewId, repo }) => {
3944
+ console.error(`Fetching review comments for PR ${number}${reviewId ? ` (review ${reviewId})` : ""}${repo ? ` from ${repo}` : ""}`);
3945
+ try {
3946
+ const provider = new GitHubIssueManagementProvider();
3947
+ const comments = await provider.getReviewComments({ number, reviewId, repo });
3948
+ console.error(`Review comments fetched successfully: ${comments.length} comments`);
3949
+ const result = { comments };
3950
+ return {
3951
+ content: [
3952
+ {
3953
+ type: "text",
3954
+ text: JSON.stringify(result)
3955
+ }
3956
+ ],
3957
+ structuredContent: result
3958
+ };
3959
+ } catch (error) {
3960
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
3961
+ console.error(`Failed to fetch review comments: ${errorMessage}`);
3962
+ throw new Error(`Failed to fetch review comments: ${errorMessage}`);
3963
+ }
3964
+ }
3965
+ );
1910
3966
  server.registerTool(
1911
3967
  "get_comment",
1912
3968
  {
1913
3969
  title: "Get Comment",
1914
3970
  description: "Fetch a specific comment by ID. Author has normalized core fields { id, displayName } plus provider-specific fields.",
1915
3971
  inputSchema: {
1916
- commentId: z.string().describe("The comment identifier to fetch"),
1917
- number: z.string().describe("The issue or PR identifier (context for providers that need it)"),
1918
- repo: z.string().optional().describe(
3972
+ commentId: z2.string().describe("The comment identifier to fetch"),
3973
+ number: z2.string().describe("The issue or PR identifier (context for providers that need it)"),
3974
+ repo: z2.string().optional().describe(
1919
3975
  'Optional repository in "owner/repo" format or full GitHub URL. When not provided, uses the current repository. GitHub only.'
1920
3976
  )
1921
3977
  },
1922
3978
  outputSchema: {
1923
- id: z.string().describe("Comment identifier"),
1924
- body: z.string().describe("Comment body content"),
3979
+ id: z2.string().describe("Comment identifier"),
3980
+ body: z2.string().describe("Comment body content"),
1925
3981
  author: flexibleAuthorSchema.nullable().describe(
1926
3982
  "Comment author with normalized { id, displayName } plus provider-specific fields"
1927
3983
  ),
1928
- created_at: z.string().describe("Comment creation timestamp"),
1929
- updated_at: z.string().optional().describe("Comment last updated timestamp")
3984
+ created_at: z2.string().describe("Comment creation timestamp"),
3985
+ updated_at: z2.string().optional().describe("Comment last updated timestamp")
1930
3986
  }
1931
3987
  },
1932
3988
  async ({ commentId, number, repo }) => {
1933
3989
  console.error(`Fetching comment ${commentId} from issue ${number}${repo ? ` in ${repo}` : ""}`);
1934
3990
  try {
1935
3991
  const provider = IssueManagementProviderFactory.create(
1936
- process.env.ISSUE_PROVIDER
3992
+ process.env.ISSUE_PROVIDER,
3993
+ settings
1937
3994
  );
1938
3995
  const result = await provider.getComment({ commentId, number, repo });
1939
3996
  console.error(`Comment fetched successfully: ${result.id}`);
@@ -1959,21 +4016,21 @@ server.registerTool(
1959
4016
  title: "Create Comment",
1960
4017
  description: "Create a new comment on an issue or pull request. Use this to start tracking a workflow phase.",
1961
4018
  inputSchema: {
1962
- number: z.string().describe("The issue or PR identifier"),
1963
- body: z.string().describe("The comment body (markdown supported)"),
1964
- type: z.enum(["issue", "pr"]).describe("Type of entity to comment on (issue or pr)")
4019
+ number: z2.string().describe("The issue or PR identifier"),
4020
+ body: z2.string().describe("The comment body (markdown supported)"),
4021
+ type: z2.enum(["issue", "pr"]).describe("Type of entity to comment on (issue or pr)")
1965
4022
  },
1966
4023
  outputSchema: {
1967
- id: z.string(),
1968
- url: z.string(),
1969
- created_at: z.string().optional()
4024
+ id: z2.string(),
4025
+ url: z2.string(),
4026
+ created_at: z2.string().optional()
1970
4027
  }
1971
4028
  },
1972
4029
  async ({ number, body, type }) => {
1973
4030
  console.error(`Creating ${type} comment on ${number}`);
1974
4031
  try {
1975
4032
  const providerType = type === "pr" ? "github" : process.env.ISSUE_PROVIDER;
1976
- const provider = IssueManagementProviderFactory.create(providerType);
4033
+ const provider = IssueManagementProviderFactory.create(providerType, settings);
1977
4034
  const result = await provider.createComment({ number, body, type });
1978
4035
  console.error(
1979
4036
  `Comment created successfully: ${result.id} at ${result.url}`
@@ -2000,22 +4057,22 @@ server.registerTool(
2000
4057
  title: "Update Comment",
2001
4058
  description: "Update an existing comment. Use this to update progress during a workflow phase.",
2002
4059
  inputSchema: {
2003
- commentId: z.string().describe("The comment identifier to update"),
2004
- number: z.string().describe("The issue or PR identifier (context for providers that need it)"),
2005
- body: z.string().describe("The updated comment body (markdown supported)"),
2006
- type: z.enum(["issue", "pr"]).optional().describe("Optional type to route PR comments to GitHub regardless of configured provider")
4060
+ commentId: z2.string().describe("The comment identifier to update"),
4061
+ number: z2.string().describe("The issue or PR identifier (context for providers that need it)"),
4062
+ body: z2.string().describe("The updated comment body (markdown supported)"),
4063
+ type: z2.enum(["issue", "pr"]).optional().describe("Optional type to route PR comments to GitHub regardless of configured provider")
2007
4064
  },
2008
4065
  outputSchema: {
2009
- id: z.string(),
2010
- url: z.string(),
2011
- updated_at: z.string().optional()
4066
+ id: z2.string(),
4067
+ url: z2.string(),
4068
+ updated_at: z2.string().optional()
2012
4069
  }
2013
4070
  },
2014
4071
  async ({ commentId, number, body, type }) => {
2015
4072
  console.error(`Updating comment ${commentId} on ${type === "pr" ? "PR" : "issue"} ${number}`);
2016
4073
  try {
2017
4074
  const providerType = type === "pr" ? "github" : process.env.ISSUE_PROVIDER;
2018
- const provider = IssueManagementProviderFactory.create(providerType);
4075
+ const provider = IssueManagementProviderFactory.create(providerType, settings);
2019
4076
  const result = await provider.updateComment({ commentId, number, body });
2020
4077
  console.error(
2021
4078
  `Comment updated successfully: ${result.id} at ${result.url}`
@@ -2042,25 +4099,26 @@ server.registerTool(
2042
4099
  title: "Create Issue",
2043
4100
  description: 'Create a new issue in the configured issue tracker. For GitHub: creates issue in the configured repository. For Linear: requires teamKey parameter (e.g., "ENG", "PLAT"), or configure issueManagement.linear.teamId in settings, or call get_issue first to auto-detect the team.',
2044
4101
  inputSchema: {
2045
- title: z.string().describe("The issue title"),
2046
- body: z.string().describe("The issue body/description (markdown supported)"),
2047
- labels: z.array(z.string()).optional().describe("Optional labels to apply to the issue"),
2048
- teamKey: z.string().optional().describe('Team key for Linear (e.g., "ENG"). Falls back to settings or team extracted from previous get_issue call. Ignored for GitHub.'),
2049
- repo: z.string().optional().describe(
4102
+ title: z2.string().describe("The issue title"),
4103
+ body: z2.string().describe("The issue body/description (markdown supported)"),
4104
+ labels: z2.array(z2.string()).optional().describe("Optional labels to apply to the issue"),
4105
+ teamKey: z2.string().optional().describe('Team key for Linear (e.g., "ENG"). Falls back to settings or team extracted from previous get_issue call. Ignored for GitHub.'),
4106
+ repo: z2.string().optional().describe(
2050
4107
  'Optional repository in "owner/repo" format or full GitHub URL. When not provided, uses the current repository. GitHub only.'
2051
4108
  )
2052
4109
  },
2053
4110
  outputSchema: {
2054
- id: z.string().describe("Issue identifier"),
2055
- url: z.string().describe("Issue URL"),
2056
- number: z.number().optional().describe("Issue number (GitHub only)")
4111
+ id: z2.string().describe("Issue identifier"),
4112
+ url: z2.string().describe("Issue URL"),
4113
+ number: z2.number().optional().describe("Issue number (GitHub only)")
2057
4114
  }
2058
4115
  },
2059
4116
  async ({ title, body, labels, teamKey, repo }) => {
2060
4117
  console.error(`Creating issue: ${title}${repo ? ` in ${repo}` : ""}`);
2061
4118
  try {
2062
4119
  const provider = IssueManagementProviderFactory.create(
2063
- process.env.ISSUE_PROVIDER
4120
+ process.env.ISSUE_PROVIDER,
4121
+ settings
2064
4122
  );
2065
4123
  const result = await provider.createIssue({ title, body, labels, teamKey, repo });
2066
4124
  console.error(`Issue created successfully: ${result.id} at ${result.url}`);
@@ -2086,26 +4144,27 @@ server.registerTool(
2086
4144
  title: "Create Child Issue",
2087
4145
  description: 'Create a new child issue linked to a parent issue. For GitHub: creates issue and links via sub-issue API (requires two API calls). For Linear: creates issue atomically with parent relationship. The parentId should be the parent issue identifier (GitHub issue number or Linear identifier like "ENG-123").',
2088
4146
  inputSchema: {
2089
- parentId: z.string().describe('Parent issue identifier (GitHub issue number or Linear identifier like "ENG-123")'),
2090
- title: z.string().describe("The child issue title"),
2091
- body: z.string().describe("The child issue body/description (markdown supported)"),
2092
- labels: z.array(z.string()).optional().describe("Optional labels to apply to the child issue"),
2093
- teamKey: z.string().optional().describe('Team key for Linear (e.g., "ENG"). Falls back to parent team. Ignored for GitHub.'),
2094
- repo: z.string().optional().describe(
4147
+ parentId: z2.string().describe('Parent issue identifier (GitHub issue number or Linear identifier like "ENG-123")'),
4148
+ title: z2.string().describe("The child issue title"),
4149
+ body: z2.string().describe("The child issue body/description (markdown supported)"),
4150
+ labels: z2.array(z2.string()).optional().describe("Optional labels to apply to the child issue"),
4151
+ teamKey: z2.string().optional().describe('Team key for Linear (e.g., "ENG"). Falls back to parent team. Ignored for GitHub.'),
4152
+ repo: z2.string().optional().describe(
2095
4153
  'Optional repository in "owner/repo" format or full GitHub URL. When not provided, uses the current repository. GitHub only.'
2096
4154
  )
2097
4155
  },
2098
4156
  outputSchema: {
2099
- id: z.string().describe("Issue identifier"),
2100
- url: z.string().describe("Issue URL"),
2101
- number: z.number().optional().describe("Issue number (GitHub only)")
4157
+ id: z2.string().describe("Issue identifier"),
4158
+ url: z2.string().describe("Issue URL"),
4159
+ number: z2.number().optional().describe("Issue number (GitHub only)")
2102
4160
  }
2103
4161
  },
2104
4162
  async ({ parentId, title, body, labels, teamKey, repo }) => {
2105
4163
  console.error(`Creating child issue for parent ${parentId}: ${title}${repo ? ` in ${repo}` : ""}`);
2106
4164
  try {
2107
4165
  const provider = IssueManagementProviderFactory.create(
2108
- process.env.ISSUE_PROVIDER
4166
+ process.env.ISSUE_PROVIDER,
4167
+ settings
2109
4168
  );
2110
4169
  const result = await provider.createChildIssue({ parentId, title, body, labels, teamKey, repo });
2111
4170
  console.error(`Child issue created successfully: ${result.id} at ${result.url}`);
@@ -2125,11 +4184,11 @@ server.registerTool(
2125
4184
  }
2126
4185
  }
2127
4186
  );
2128
- var dependencyResultSchema = z.object({
2129
- id: z.string().describe("Issue identifier"),
2130
- title: z.string().describe("Issue title"),
2131
- url: z.string().describe("Issue URL"),
2132
- state: z.string().describe("Issue state")
4187
+ var dependencyResultSchema = z2.object({
4188
+ id: z2.string().describe("Issue identifier"),
4189
+ title: z2.string().describe("Issue title"),
4190
+ url: z2.string().describe("Issue URL"),
4191
+ state: z2.string().describe("Issue state")
2133
4192
  });
2134
4193
  server.registerTool(
2135
4194
  "create_dependency",
@@ -2137,21 +4196,22 @@ server.registerTool(
2137
4196
  title: "Create Dependency",
2138
4197
  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.',
2139
4198
  inputSchema: {
2140
- blockingIssue: z.string().describe('The issue that blocks (GitHub issue number or Linear identifier like "ENG-123")'),
2141
- blockedIssue: z.string().describe('The issue being blocked (GitHub issue number or Linear identifier like "ENG-123")'),
2142
- repo: z.string().optional().describe(
4199
+ blockingIssue: z2.string().describe('The issue that blocks (GitHub issue number or Linear identifier like "ENG-123")'),
4200
+ blockedIssue: z2.string().describe('The issue being blocked (GitHub issue number or Linear identifier like "ENG-123")'),
4201
+ repo: z2.string().optional().describe(
2143
4202
  'Optional repository in "owner/repo" format or full GitHub URL. When not provided, uses the current repository. GitHub only.'
2144
4203
  )
2145
4204
  },
2146
4205
  outputSchema: {
2147
- success: z.boolean().describe("Whether the dependency was created successfully")
4206
+ success: z2.boolean().describe("Whether the dependency was created successfully")
2148
4207
  }
2149
4208
  },
2150
4209
  async ({ blockingIssue, blockedIssue, repo }) => {
2151
4210
  console.error(`Creating dependency: ${blockingIssue} blocks ${blockedIssue}${repo ? ` in ${repo}` : ""}`);
2152
4211
  try {
2153
4212
  const provider = IssueManagementProviderFactory.create(
2154
- process.env.ISSUE_PROVIDER
4213
+ process.env.ISSUE_PROVIDER,
4214
+ settings
2155
4215
  );
2156
4216
  await provider.createDependency({ blockingIssue, blockedIssue, repo });
2157
4217
  console.error(`Dependency created successfully: ${blockingIssue} -> ${blockedIssue}`);
@@ -2177,22 +4237,23 @@ server.registerTool(
2177
4237
  title: "Get Dependencies",
2178
4238
  description: "Get blocking/blocked_by dependencies for an issue. Returns lists of issues that this issue blocks and/or is blocked by.",
2179
4239
  inputSchema: {
2180
- number: z.string().describe('Issue identifier (GitHub issue number or Linear identifier like "ENG-123")'),
2181
- 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'),
2182
- repo: z.string().optional().describe(
4240
+ number: z2.string().describe('Issue identifier (GitHub issue number or Linear identifier like "ENG-123")'),
4241
+ direction: z2.enum(["blocking", "blocked_by", "both"]).describe('Which dependencies to fetch: "blocking" for issues this blocks, "blocked_by" for issues blocking this, "both" for all'),
4242
+ repo: z2.string().optional().describe(
2183
4243
  'Optional repository in "owner/repo" format or full GitHub URL. When not provided, uses the current repository. GitHub only.'
2184
4244
  )
2185
4245
  },
2186
4246
  outputSchema: {
2187
- blocking: z.array(dependencyResultSchema).describe("Issues that this issue blocks"),
2188
- blockedBy: z.array(dependencyResultSchema).describe("Issues that block this issue")
4247
+ blocking: z2.array(dependencyResultSchema).describe("Issues that this issue blocks"),
4248
+ blockedBy: z2.array(dependencyResultSchema).describe("Issues that block this issue")
2189
4249
  }
2190
4250
  },
2191
4251
  async ({ number, direction, repo }) => {
2192
4252
  console.error(`Getting dependencies for ${number} (direction: ${direction})${repo ? ` in ${repo}` : ""}`);
2193
4253
  try {
2194
4254
  const provider = IssueManagementProviderFactory.create(
2195
- process.env.ISSUE_PROVIDER
4255
+ process.env.ISSUE_PROVIDER,
4256
+ settings
2196
4257
  );
2197
4258
  const result = await provider.getDependencies({ number, direction, repo });
2198
4259
  console.error(`Dependencies fetched: ${result.blocking.length} blocking, ${result.blockedBy.length} blocked_by`);
@@ -2218,21 +4279,22 @@ server.registerTool(
2218
4279
  title: "Remove Dependency",
2219
4280
  description: "Remove a blocking dependency between two issues. The blockingIssue will no longer block the blockedIssue.",
2220
4281
  inputSchema: {
2221
- blockingIssue: z.string().describe('The issue that blocks (GitHub issue number or Linear identifier like "ENG-123")'),
2222
- blockedIssue: z.string().describe('The issue being blocked (GitHub issue number or Linear identifier like "ENG-123")'),
2223
- repo: z.string().optional().describe(
4282
+ blockingIssue: z2.string().describe('The issue that blocks (GitHub issue number or Linear identifier like "ENG-123")'),
4283
+ blockedIssue: z2.string().describe('The issue being blocked (GitHub issue number or Linear identifier like "ENG-123")'),
4284
+ repo: z2.string().optional().describe(
2224
4285
  'Optional repository in "owner/repo" format or full GitHub URL. When not provided, uses the current repository. GitHub only.'
2225
4286
  )
2226
4287
  },
2227
4288
  outputSchema: {
2228
- success: z.boolean().describe("Whether the dependency was removed successfully")
4289
+ success: z2.boolean().describe("Whether the dependency was removed successfully")
2229
4290
  }
2230
4291
  },
2231
4292
  async ({ blockingIssue, blockedIssue, repo }) => {
2232
4293
  console.error(`Removing dependency: ${blockingIssue} blocks ${blockedIssue}${repo ? ` in ${repo}` : ""}`);
2233
4294
  try {
2234
4295
  const provider = IssueManagementProviderFactory.create(
2235
- process.env.ISSUE_PROVIDER
4296
+ process.env.ISSUE_PROVIDER,
4297
+ settings
2236
4298
  );
2237
4299
  await provider.removeDependency({ blockingIssue, blockedIssue, repo });
2238
4300
  console.error(`Dependency removed successfully: ${blockingIssue} -> ${blockedIssue}`);
@@ -2252,11 +4314,11 @@ server.registerTool(
2252
4314
  }
2253
4315
  }
2254
4316
  );
2255
- var childIssueResultSchema = z.object({
2256
- id: z.string().describe("Issue identifier"),
2257
- title: z.string().describe("Issue title"),
2258
- url: z.string().describe("Issue URL"),
2259
- state: z.string().describe("Issue state")
4317
+ var childIssueResultSchema = z2.object({
4318
+ id: z2.string().describe("Issue identifier"),
4319
+ title: z2.string().describe("Issue title"),
4320
+ url: z2.string().describe("Issue URL"),
4321
+ state: z2.string().describe("Issue state")
2260
4322
  });
2261
4323
  server.registerTool(
2262
4324
  "get_child_issues",
@@ -2264,20 +4326,21 @@ server.registerTool(
2264
4326
  title: "Get Child Issues",
2265
4327
  description: "Get child issues (sub-issues) of a parent issue. Returns a list of issues that are children of the specified parent.",
2266
4328
  inputSchema: {
2267
- number: z.string().describe('Parent issue identifier (GitHub issue number or Linear identifier like "ENG-123")'),
2268
- repo: z.string().optional().describe(
4329
+ number: z2.string().describe('Parent issue identifier (GitHub issue number or Linear identifier like "ENG-123")'),
4330
+ repo: z2.string().optional().describe(
2269
4331
  'Optional repository in "owner/repo" format or full GitHub URL. When not provided, uses the current repository. GitHub only.'
2270
4332
  )
2271
4333
  },
2272
4334
  outputSchema: {
2273
- children: z.array(childIssueResultSchema).describe("Child issues of the parent")
4335
+ children: z2.array(childIssueResultSchema).describe("Child issues of the parent")
2274
4336
  }
2275
4337
  },
2276
4338
  async ({ number, repo }) => {
2277
4339
  console.error(`Getting child issues for ${number}${repo ? ` in ${repo}` : ""}`);
2278
4340
  try {
2279
4341
  const provider = IssueManagementProviderFactory.create(
2280
- process.env.ISSUE_PROVIDER
4342
+ process.env.ISSUE_PROVIDER,
4343
+ settings
2281
4344
  );
2282
4345
  const result = await provider.getChildIssues({ number, repo });
2283
4346
  console.error(`Child issues fetched: ${result.length} children`);
@@ -2297,8 +4360,160 @@ server.registerTool(
2297
4360
  }
2298
4361
  }
2299
4362
  );
4363
+ server.registerTool(
4364
+ "close_issue",
4365
+ {
4366
+ title: "Close Issue",
4367
+ description: 'Close an issue in the configured issue tracker. For GitHub: uses `gh issue close`. For Linear: transitions issue to "Done" state. For Jira: transitions issue to "Done" state.',
4368
+ inputSchema: {
4369
+ number: z2.string().describe("The issue identifier"),
4370
+ repo: z2.string().optional().describe(
4371
+ 'Optional repository in "owner/repo" format or full GitHub URL. When not provided, uses the current repository. GitHub only.'
4372
+ )
4373
+ },
4374
+ outputSchema: {
4375
+ success: z2.boolean().describe("Whether the issue was closed successfully")
4376
+ }
4377
+ },
4378
+ async ({ number, repo }) => {
4379
+ console.error(`Closing issue ${number}${repo ? ` in ${repo}` : ""}`);
4380
+ try {
4381
+ const provider = IssueManagementProviderFactory.create(
4382
+ process.env.ISSUE_PROVIDER,
4383
+ settings
4384
+ );
4385
+ await provider.closeIssue({ number, repo });
4386
+ console.error(`Issue closed successfully: ${number}`);
4387
+ return {
4388
+ content: [
4389
+ {
4390
+ type: "text",
4391
+ text: JSON.stringify({ success: true })
4392
+ }
4393
+ ],
4394
+ structuredContent: { success: true }
4395
+ };
4396
+ } catch (error) {
4397
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
4398
+ console.error(`Failed to close issue: ${errorMessage}`);
4399
+ throw new Error(`Failed to close issue: ${errorMessage}`);
4400
+ }
4401
+ }
4402
+ );
4403
+ server.registerTool(
4404
+ "reopen_issue",
4405
+ {
4406
+ title: "Reopen Issue",
4407
+ description: 'Reopen a closed issue in the configured issue tracker. For GitHub: uses `gh issue reopen`. For Linear: transitions issue to "Todo" state. For Jira: transitions issue to "Reopen" or "To Do" state.',
4408
+ inputSchema: {
4409
+ number: z2.string().describe("The issue identifier"),
4410
+ repo: z2.string().optional().describe(
4411
+ 'Optional repository in "owner/repo" format or full GitHub URL. When not provided, uses the current repository. GitHub only.'
4412
+ )
4413
+ },
4414
+ outputSchema: {
4415
+ success: z2.boolean().describe("Whether the issue was reopened successfully")
4416
+ }
4417
+ },
4418
+ async ({ number, repo }) => {
4419
+ console.error(`Reopening issue ${number}${repo ? ` in ${repo}` : ""}`);
4420
+ try {
4421
+ const provider = IssueManagementProviderFactory.create(
4422
+ process.env.ISSUE_PROVIDER,
4423
+ settings
4424
+ );
4425
+ await provider.reopenIssue({ number, repo });
4426
+ console.error(`Issue reopened successfully: ${number}`);
4427
+ return {
4428
+ content: [
4429
+ {
4430
+ type: "text",
4431
+ text: JSON.stringify({ success: true })
4432
+ }
4433
+ ],
4434
+ structuredContent: { success: true }
4435
+ };
4436
+ } catch (error) {
4437
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
4438
+ console.error(`Failed to reopen issue: ${errorMessage}`);
4439
+ throw new Error(`Failed to reopen issue: ${errorMessage}`);
4440
+ }
4441
+ }
4442
+ );
4443
+ server.registerTool(
4444
+ "edit_issue",
4445
+ {
4446
+ title: "Edit Issue",
4447
+ description: "Edit an issue's properties (title, body, state, labels) in the configured issue tracker. State changes use close/reopen internally. For GitHub: uses `gh issue edit` for field updates and `gh issue close/reopen` for state. For Linear: uses Linear SDK to update fields and state transitions. For Jira: uses REST API to update fields and transitions for state.",
4448
+ inputSchema: {
4449
+ number: z2.string().describe("The issue identifier"),
4450
+ title: z2.string().optional().describe("New issue title"),
4451
+ body: z2.string().optional().describe("New issue body/description"),
4452
+ state: z2.enum(["open", "closed"]).optional().describe("New issue state"),
4453
+ labels: z2.array(z2.string()).optional().describe("Labels to add to the issue"),
4454
+ repo: z2.string().optional().describe(
4455
+ 'Optional repository in "owner/repo" format or full GitHub URL. When not provided, uses the current repository. GitHub only.'
4456
+ )
4457
+ },
4458
+ outputSchema: {
4459
+ success: z2.boolean().describe("Whether the issue was edited successfully")
4460
+ }
4461
+ },
4462
+ async ({ number, title, body, state, labels, repo }) => {
4463
+ console.error(`Editing issue ${number}${repo ? ` in ${repo}` : ""}`);
4464
+ try {
4465
+ const provider = IssueManagementProviderFactory.create(
4466
+ process.env.ISSUE_PROVIDER,
4467
+ settings
4468
+ );
4469
+ await provider.editIssue({ number, title, body, state, labels, repo });
4470
+ console.error(`Issue edited successfully: ${number}`);
4471
+ return {
4472
+ content: [
4473
+ {
4474
+ type: "text",
4475
+ text: JSON.stringify({ success: true })
4476
+ }
4477
+ ],
4478
+ structuredContent: { success: true }
4479
+ };
4480
+ } catch (error) {
4481
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
4482
+ console.error(`Failed to edit issue: ${errorMessage}`);
4483
+ throw new Error(`Failed to edit issue: ${errorMessage}`);
4484
+ }
4485
+ }
4486
+ );
2300
4487
  async function main() {
2301
- console.error("Starting Issue Management MCP Server...");
4488
+ console.error("=== Issue Management MCP Server Starting ===");
4489
+ console.error(`PID: ${process.pid}`);
4490
+ console.error(`Node version: ${process.version}`);
4491
+ console.error(`CWD: ${process.cwd()}`);
4492
+ console.error(`Script: ${new URL(import.meta.url).pathname}`);
4493
+ const relevantEnvKeys = [
4494
+ "ISSUE_PROVIDER",
4495
+ "REPO_OWNER",
4496
+ "REPO_NAME",
4497
+ "GITHUB_API_URL",
4498
+ "GITHUB_EVENT_NAME",
4499
+ "DRAFT_PR_NUMBER",
4500
+ "LINEAR_API_TOKEN",
4501
+ "LINEAR_TEAM_KEY",
4502
+ "JIRA_HOST",
4503
+ "JIRA_USERNAME",
4504
+ "JIRA_API_TOKEN",
4505
+ "JIRA_PROJECT_KEY"
4506
+ ];
4507
+ console.error("Environment variables:");
4508
+ for (const key of relevantEnvKeys) {
4509
+ const val = process.env[key];
4510
+ if (val !== void 0) {
4511
+ console.error(` ${key}=${val}`);
4512
+ }
4513
+ }
4514
+ const settingsManager = new SettingsManager();
4515
+ settings = await settingsManager.loadSettings();
4516
+ console.error("Settings loaded");
2302
4517
  const provider = validateEnvironment();
2303
4518
  console.error("Environment validated");
2304
4519
  console.error(`Issue management provider: ${provider}`);
@@ -2308,7 +4523,7 @@ async function main() {
2308
4523
  }
2309
4524
  const transport = new StdioServerTransport();
2310
4525
  await server.connect(transport);
2311
- console.error("Issue Management MCP Server running on stdio transport");
4526
+ console.error("=== Issue Management MCP Server READY (stdio transport) ===");
2312
4527
  }
2313
4528
  main().catch((error) => {
2314
4529
  console.error("Fatal error starting MCP server:", error);